本次分析过程并非没有人受伤。
前言 — 一言难尽
新版的“某某校园”引入了大量抽象 anti‑cheat 机制,覆盖了多开、模拟器、虚拟机、so 注入、hook、调试器附加、SSL pinning 等多种检测,竭尽全力无法绕过
下面的内容以技术分析为主,请仅用于学习与研究,不要用于非法用途。
流程
抓包
该 app 启用了严格的 SSL pinning 策略。几乎无法直接绕过。
思路:使用旧版本的 APK、重写覆盖版本检测api。使用的是 reqable
(自带 Py脚本支持,类似 Fiddler 的局域网抓包,还有Postman的api测试)非常推荐
抓包后会发现大量接口返回的数据是加密过的,同时请求中带有
sign
字段 —— 于是开始逆向 app,破解签名/加解密算法。
脱壳解包
使用 jadx 等工具查看 dex
抽取加/解密密钥与算法实现,验证并复现加密/解密与签名逻辑。
下面给出部分逆向过程(旧版本 dex,但经过验证及对新版本逆向,逻辑与新版相同):
网易易盾的壳,https://56.al 可在线脱壳 有实力的🈸可自行手撕
有趣的是,这个网站根据哈希判断,我上传上去 apk 后就秒脱了,意味着有人已经脱过了,看记录是在九月份
开逆
找到添加Sign
请求头的代码
查找相关用例 跟进
签名/加解密流程
理解完整的签名流程
获取密钥
跟进获取密钥的函数 看到判断
(x7.d.b() == 9 || x7.d.b() == 11) ? "F44B0282BEA83557" : "huachenjie"
x7.d.b()
用于判断运行环境,初始化值为 9,因此选中的解密密钥为 F44B0282BEA83557
。
继续跟进相关函数,可以找到用于数据加密的第二个密钥与其他配套逻辑。
复现算法(Python)
根据逆向结果整理并复现核心算法:
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib
# ---------------- AES 工具 ----------------
IV = b"01234ABCDEF56789"
key_str = "F44B0282BEA83557"
def c(str2: str) -> bytes:
"""生成 32 字节 AES key"""
if str2 is None:
return None
b = str2.encode('utf-8')
if len(b) == 32:
return b
b_arr = bytearray(32)
b_arr[:min(len(b), 32)] = b[:32]
return bytes(b_arr)
def encrypt_aes(plaintext: str) -> str:
key_bytes = c(key_str)
cipher = AES.new(key_bytes, AES.MODE_CBC, IV)
ciphertext = cipher.encrypt(pad(plaintext.encode('utf-8'), AES.block_size, style='pkcs7'))
return base64.b64encode(ciphertext).decode('utf-8')
def decrypt_aes(cipher_b64: str) -> str:
key_bytes = c(key_str)
cipher = AES.new(key_bytes, AES.MODE_CBC, IV)
ct_bytes = base64.b64decode(cipher_b64)
pt_bytes = unpad(cipher.decrypt(ct_bytes), AES.block_size, style='pkcs7')
return pt_bytes.decode('utf-8')
# ---------------- 哈希工具 ----------------
def obfuscated_hash(s: str, algo='sha256') -> str:
"""自定义 hash"""
if not s:
return ""
h = hashlib.new(algo)
h.update(s.encode('utf-8'))
hexstr = h.hexdigest()
if len(hexstr) < 16:
return hexstr
return hexstr[-8:] + hexstr[8:len(hexstr)-8] + hexstr[:8]
# ---------------- 签名计算 ----------------
def calculate_sign(data: str) -> str:
"""先哈希原文,再 AES 加密生成 sign"""
hashed = obfuscated_hash(data) # 先哈希
sign = encrypt_aes(hashed) # AES 加密 hash
return sign
接口
抓包过程中发现若干 API
“阳光跑”接口
querySchoolFences
查询学校电子围栏信息,包括经纬度范围、开放时间、打卡点数量、面部识别要求等。
电子围栏为一个数组,具有至少三个经纬度信息点作为围栏
checkSunRunConfig
检查跑步计划配置,包括打卡方式、围栏、面部识别要求等信息
querySunRunAbstractInfoV2
获取指定阳光跑计划的汇总信息,同时下发学校要求的相关规定,包括但不限于禁止跑步时连接WIFI
uploadRunRecord
每 3 秒采样一次(包括围栏距离、经纬度、卫星信息等),并每 15 秒通过 uploadRunRecord
上报一次——一次上报包含这 15 秒内采样的 5 个点的数据。
finishSunRun
结束一次跑步,并上传跑步的最终数据(步数、距离、轨迹、配速等),用于计算成绩并生成跑步记录。
该 app 后台会通过该包上传的相关数据来进行作弊判断和数据分析,但不仅限于此
如果 app 中途异常退出数据丢失,会先通过 queryUnFinishRun
查询未完成的运动记录,然后通过 abnormalFinishSunRun
接口来结束异常运动,数据将不会被记录
其他接口大家自行探索吧,方法都摆在这里了,不过多赘述
需要注意的是,该app后台有着严格的反作弊检测,小心测试......
声明:本文仅用于技术分享与研究,禁止用于破坏、侵入、或其他违法活动。