0%

今日校园自动签到

本文将探讨如何对今日校园发起的签到进行定时回应,即自动签到,同时了解使用Python进行阿里云oss的文件上传、Python的DES加解密库、逆向分析Android应用获取关键信息(本文涉及DES加解密的密钥和IV向量的获取)等

前言

在上一篇文章中,我们讨论了如何进行今日校园的自诊打卡,但是后面经过测试发现,如果手机端重新登录,则原有的Cookie将失效,需要重新更新Cookie,所以最好在手机端登录后将Cookie填入程序里后,不要再退出登录,这样自动打卡就可以一直进行。而这篇今日校园自动签到,将不存在这一问题。当Cookie已经失效时,将自动进行重新登陆以更新Cookie,同时还可自定义签到位置,使你一直在其签到范围内

分析过程

阿里云OSS上传文件

今日校园里面的签到很多时候都是需要自拍的,而这些图片会被今日校园保存到阿里云OSS中,所以我们必须实现Python使用阿里云OSS的过程

  1. pip install oss2

  2. 你需要有如下数据:

    access_key_idaccess_key_secretendPointbucket

    本文的这些数据均通过抓包获得

  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
    def uploadPic(session):
    """
    上传图片到阿里云oss
    url: 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/getStsAccess' # 用于获取主要参数
    method: POST
    :param session: requests.session()
    :return: oss上的filename
    """
    # 抓包获取文件上传所需信息
    url = 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/getStsAccess'
    res = session.post(url=url, verify=False)
    datas = res.json().get('datas')
    # 所需信息
    filename = datas.get('fileName')
    accessKeyId = datas.get('accessKeyId')
    accessSecret = datas.get('accessKeySecret')
    securityToken = datas.get('securityToken')
    endPoint = datas.get('endPoint')
    bucket = datas.get('bucket')
    # 创建对象
    bucket = oss2.Bucket(oss2.Auth(access_key_id=accessKeyId, access_key_secret=accessSecret), endPoint, bucket)
    # 读取home.jpg并上传到oss上的filename
    with open('home.jpg', "rb") as f:
    data = f.read()
    bucket.put_object(key=filename, headers={'x-oss-security-token': securityToken}, data=data)
    # 返回值为链接,参数依次为,方法、oss上文件路径、过期时间(s)
    ret = bucket.sign_url('PUT', filename, 60)
    return filename

DES加解密

反编译该App的部分代码可以看出,今日校园许多关键信息均使用DES加密,且加密所需的IV向量为byte类型的数组{1,2,3,4,5,6,7,8}

java-1

所以,我们提交这些信息也必须经过加密,而DES加密的密钥也能从相应的代码中分析得到:ST83=@XV

java-2

在最新的v8.1.12更新日志中,提到了对软件的安全机制进行了加强:

update-log

经过分析,发现确实如此,今日校园App开始使用360加固进行安全防护,但在我之前使用的v8.1.7中,是可以直接分析其没有经过加固的代码的。而对于360加固过的应用如何进行逆向分析不是本文的重点,有机会将在以后的文章中进行讨论如何对其脱壳

所以在得到这些关键信息后就可进行DES加密部分的代码编写了:

1
2
3
4
5
6
7
8
# pip install pyDes
def encrypt(s, key='ST83=@XV'):
import pyDes
key = key
iv = b"\x01\x02\x03\x04\x05\x06\x07\x08"
k = des(key, CBC, iv, pad=None, padmode=PAD_PKCS5)
EncryptStr = k.encrypt(s)
return base64.b64encode(EncryptStr).decode()

其他文件

  1. home.jpg

    你需要一张你的签到图片home.jpg,以便在签到时上传该文件

完整代码

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
import base64
import json
import re
import time

import oss2
import requests
import urllib3
from pyDes import *
from requests.utils import dict_from_cookiejar

urllib3.disable_warnings()

session = requests.session()
session.headers = {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Linux; Android 8.1.0; 16th Build/OPM1.171019.026; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/65.0.3325.110 Mobile Safari/537.36 yiban/8.1.7 cpdaily/8.1.7 wisedu/8.1.7',
}

# 此处配置你的账号
USERCODE = '201707060000'
# 此处配置你的密码
USERPWD = '123456'
# 此处填写最新的App版本
APP_VERSION = '8.1.12'

# 签到模式 custom:自定义位置;auto: 自动获取第一个签到范围内的位置
MOD = 'custom'

# 此处填写你的签到地址信息
POSITION = '中国山东省潍坊市寿光市'

# 当签到模式为custom时有效
# 此处填写地址经纬度
LON = 118.785300
LAT = 36.889072
# 如果准备签到的位置在签到范围外,则在此处填写原因
REASON = '原因'


# des加密
def encrypt(s, key='ST83=@XV'):
key = key
iv = b"\x01\x02\x03\x04\x05\x06\x07\x08"
k = des(key, CBC, iv, pad=None, padmode=PAD_PKCS5)
EncryptStr = k.encrypt(s)
return base64.b64encode(EncryptStr).decode() # 转base64编码返回


def createCpdailyInfo(lon, lat, open_id):
"""
headers中的CpdailyInfo参数
:param lon: 定位经度
:param lat: 定位纬度
:param open_id: 学生学号
:return: CpdailyInfo
"""
s = r'{"systemName":"android","systemVersion":"8.1.0","model":"16th",' \
r'"deviceId":"ffd1df5b-e69a-4938-9624-38d575039f83","appVersion":"8.1.11","lon":' + str(
lon) + ',"lat":' + str(lat) + ',"userId":"' + open_id + '"}'
info = encrypt(s)
return info


def uploadPic():
"""
上传图片到阿里云oss
url: 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/getStsAccess'
method: POST
:return: oss上的filename
"""
url = 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/getStsAccess'
res = session.post(url=url, verify=False)
datas = res.json().get('datas')
filename = datas.get('fileName')
accessKeyId = datas.get('accessKeyId')
accessSecret = datas.get('accessKeySecret')
securityToken = datas.get('securityToken')
endPoint = datas.get('endPoint')
bucket = datas.get('bucket')
# 创建对象
bucket = oss2.Bucket(oss2.Auth(access_key_id=accessKeyId, access_key_secret=accessSecret), endPoint, bucket)
# 读取test.jpg并上传到oss上的filename
with open('home.jpg', "rb") as f:
data = f.read()
bucket.put_object(key=filename, headers={'x-oss-security-token': securityToken}, data=data)
# 返回值为链接,参数依次为,方法、oss上文件路径、过期时间(s)
ret = bucket.sign_url('PUT', filename, 60)
return filename


def getSignInfoInOneDay():
"""
:url: https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/getStuSignInfosInOneDay
:method: POST
:data: {}
"""
url = 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/getStuSignInfosInOneDay'
data = json.dumps({})
res = session.post(url=url, data=data, allow_redirects=False, verify=False)
if res.status_code == 302:
print('登录过期')
return None
datas = res.json().get('datas', {})
signedTasks = datas.get('signedTasks')
unSignedTasks = datas.get('unSignedTasks')

tasks = {'signedTasks': signedTasks, 'unSignedTasks': unSignedTasks}
return tasks


def getSignDetail(us_task):
"""
签到任务详情
url: https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/detailSignInstance
method: POST
:param us_task: 未签到的任务
data: {
signInstanceWid: signInstanceWid,
signWid: signWid
}
data-type: json
"""
url = 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/detailSignInstance'
data = json.dumps({
'signInstanceWid': us_task.get('signInstanceWid'),
'signWid': us_task.get('signWid'),
})
res = session.post(url=url, data=data, verify=False)
unSignedTaskDetail = res.json().get('datas')
return unSignedTaskDetail


def getPhotoUrl(filename):
"""
获取图片上传位置
url: 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/previewAttachment'
method: POST
:param filename: 文件路径
data: {ossKey: 文件路径}
data-type: json
:return: 图片上传位置
"""
url = 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/previewAttachment'
data = json.dumps({
'ossKey': filename
})
res = session.post(url=url, data=data, verify=False)
photoUrl = res.json().get('datas')
return photoUrl


def submitSign(wid, lon, lat, reason, photo_url, position):
"""
提交签到
url: 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/submitSign'
method: POST
:param wid: 任务id string
:param lon: 经度 float
:param lat: 纬度 float
:param reason: 补充原因 string
:param photo_url: 签到图片url string
:param position: 位置信息 string
:return:
"""
url = 'https://wfust.cpdaily.com/wec-counselor-sign-apps/stu/sign/submitSign'
data = json.dumps({
'signInstanceWid': wid,
'longitude': lon,
'latitude': lat,
'isMalposition': 1,
'abnormalReason': reason,
'signPhotoUrl': photo_url,
'position': position
})
cpdaily_extension = createCpdailyInfo(lon=lon, lat=lat, open_id=USERCODE)
session.headers['Content-Type'] = 'application/json;charset=UTF-8'
res = session.post(url=url, headers={'Cpdaily-Extension': cpdaily_extension}, data=data, verify=False)
message = res.json().get('message')
return message


def startSign():
"""
签到流程控制
:return:
"""
tasks = getSignInfoInOneDay()
text = ''
if tasks is None:
if reLogin():
print('==>重新登陆成功')
text += '==>重新登陆成功'
tasks = getSignInfoInOneDay()
else:
text += '==>账号或密码错误、或者需要验证码'
return text
unSignedTasks = tasks.get('unSignedTasks')
if unSignedTasks:
print('当前有{}条签到'.format(len(unSignedTasks)))
print('开始签到')
for unSignedTask in unSignedTasks:
unSignedDetailTask = getSignDetail(us_task=unSignedTask)
# 判断是否在签到时间
currentTime = unSignedDetailTask.get('currentTime')
taskDate = unSignedDetailTask.get('rateSignDate')[0:10]
taskStartTime = unSignedDetailTask.get('rateTaskBeginTime')
taskEndTime = unSignedDetailTask.get('rateTaskEndTime')
dt1 = '{} {}'.format(taskDate, taskStartTime)
dt2 = '{} {}'.format(taskDate, taskEndTime)
timeArray1 = time.strptime(dt1, '%Y-%m-%d %H:%M')
timeArray2 = time.strptime(dt2, '%Y-%m-%d %H:%M')
timeArray3 = time.strptime(currentTime, '%Y-%m-%d %H:%M:%S')
timestamp1 = time.mktime(timeArray1)
timestamp2 = time.mktime(timeArray2)
timestamp3 = time.mktime(timeArray3)
if timestamp3 <= timestamp1 or timestamp3 >= timestamp2:
print("未到签到时间")
text += '==>未到签到时间'
break
filename = uploadPic()
photo_url = getPhotoUrl(filename=filename)
# 地址信息
reason = ''
if MOD == 'custom':
longitude = LON
latitude = LAT
reason = REASON
else:
place = unSignedDetailTask.get('signPlaceSelected')[0]
longitude = place.get('longitude')
latitude = place.get('latitude')
text += "==>" + submitSign(wid=unSignedDetailTask.get('signInstanceWid'), lon=longitude, lat=latitude, reason=reason, photo_url=photo_url, position=POSITION)
session.get('https://sc.ftqq.com/**********你server酱的SCKEY**********.send?text=今日校园签到&desp=' + text)
return text
else:
print("暂时没有签到任务")
return '==>暂时没有签到任务'


def reLogin():
"""
重新登陆
:return: 成功True/失败False
"""
url = 'https://wfust.cpdaily.com/iap/doLogin' # POST
lt_url = 'https://wfust.cpdaily.com/iap/security/lt' # POST
doLogin_headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
'Referer': 'https://wfust.cpdaily.com/iap/login/pc.html'
}
redirect_headers = {
'Referer': 'https://wfust.cpdaily.com/wec-amp-portal/login.html',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'
}
lt_data = {
'lt': ''
}
lt = session.post(url=lt_url, data=lt_data, verify=False).json().get('result').get('_lt')
data = {
# 此处配置你的账号
'username': USERCODE,
# 此处配置你的密码
'password': USERPWD,
'mobile': '',
'dllt': '',
'lt': lt,
'captcha': '',
'rememberMe': 'false'
}
# 更新部分cookie
session.post(url=url, headers=doLogin_headers, data=data, verify=False)
# login
resp = session.get(url='https://wfust.cpdaily.com/wec-amp-portal/login',
headers=redirect_headers, allow_redirects=False, verify=False)
location = resp.headers['Location']
# service
resp = session.get(url=location,
headers=redirect_headers, allow_redirects=False, verify=False)
location = resp.headers['Location']
# ticket
resp = session.get(url=location,
headers=redirect_headers, allow_redirects=False, verify=False)
dict_of_cookies = dict_from_cookiejar(session.cookies)
c_key = dict_of_cookies.get('MOD_AUTH_CAS')
if c_key:
return True
return False


if __name__ == '__main__':
text = startSign()
session.get(
'https://sc.ftqq.com/**********你server酱的SCKEY**********.send?text=今日校园签到&desp=' + text)

在云函数中部署

具体步骤参考上一篇文章:Python+云函数实现今日校园每日自诊打卡

需要注意的是,你需要在云函数里安装依赖包,当然直接把依赖包打包上传也可以。除此之外,你还可以在服务器里面部署定时任务

最后

关于上一篇文章挖的几个坑,我还没有填完,待我时间充裕或者我真的需要的时候再填吧!2020.03.31更新:已填完所有坑

国内疫情已经基本稳定,估计开学那天已经快来了!

更新

  • 2020.5.24

    更新重新登陆部分代码,适配新版应用登陆机制

  • 2020.9.26

    今日校园登陆机制改变,请自行抓包分析!

您的支持将鼓励我的创作!

欢迎关注我的其它发布渠道