疫情又反复了,学校又开始要求打卡了。可最近的阴间作息(3 点睡 12 点起)让我实在难以完成学校的要求:每天 12 点前上报健康状况。所以只能寻求自动化手段了。
在网上稍微搜寻,发现已有前辈做的研究了,Github 仓库。但是原作者已删库跑路,易班的 API 似乎也有更新,打卡的表单也有修改,所以我们还需要稍作修改。
1. 表单更新
观察源代码,得大体思路是模拟登录后拿到 token,使用 token 调用查询 “未完成任务” 列表的接口,从结果中查找指定类型的任务,拿到表单 ID。再向另一个接口提交表单 ID 与提交的数据,则任务完成。
其中关键的控制表单内容的部分如下:
dict_form = {"2fca911d0600717cc5c2f57fc3702787": [" 湖南省 ", " 长沙市 ", " 天心区 "],
"cab886bf693f23a34ed78ed71deaadc3": yb.name,
"b418fa886b6a38bdce72569a70b1fa10": ["36.2", "36.3", "36.4", "36.5", "36.6", "36.7", "36.8"][random.randint(0, 6)], # 随机体温
"c77d35b16fb22ec70a1f33c315141dbb": util.get_time_no_second()}
很明显,每项数据的字典就是字段 ID,后者就是字段的数据。我们的表单结构更新只需要拿到新版表单的字段 ID,对应修改就行。
那么要拿到表单 ID,就需要拿到表单的源代码。为了避开移动端抓 HTTPS 包的困难(但是后面还是没有避得开),这里采用 Fiddler 抓 Windows 微信,用电脑版微信访问易班校本化的微信公众号平台。
模拟人工完成一次打卡,抓到一系列的 API 包,分析打卡过程如下:
获得未完成的任务列表,每个任务有一个 TaskID
根据 TaskID 拿到表单模板 ID(WFid)
请求拿到表单模板的各个字段名称,ID,类型等等。(模拟提交的程序不需要这个接口)
将表单的数据与对应的任务 ID POST 提交回去。
与前辈的代码对比,发现表单提交的部分并没有改动,相应的修改了表单结构替换代码即可:
dict_form = {"2fca911d0600717cc5c2f57fc3702787": [" 湖南省 ", "123", "456"],
"db19a5f588037889a2ff5b6894a303a9":" 绿码 ",
"19ba06c51ab8eee843a235ece7064865":" 第二针 ",
"2d4135d558f849e18a5dcc87b884cce5": "36", # 随机体温
"c77d35b16fb22ec70a1f33c315141dbb": util.get_time_no_second()}
2. 接口变更
改好后试了一下,发现报登录失败,但是检查了账号信息是没有错误的。
查看前辈代码中登录相关的部分,发现是登录的 API 过期了,如图一所示。代码如下:
def login(self):
params = {
"mobile": self.account,
"imei": "0",
"mobile": self.passwd
}
# 最新不需要加密密码直接登录的接口来自我 B 站视频评论用户:破损的鞘翅 (bilibili_id:45807603)
r = self.request(url="https://mobile.yiban.cn/api/v3/passport/login", params=params)
if r is not None and str(r["response"]) == "100":
self.access_token = r["data"]["user"]["access_token"]
return r
else:
raise Exception(" 账号或密码错误 ")
试了一下版本号,发现应该是 v4 了。但是麻烦的是,v4 的登录接口有变动,方法变为了 POST,字段至少还需要一个 “ct”,而该字段的含义就不清楚了,所以在这里我想着在易班的产品上再找找机会。
整理一下思路:
我们的目的是拿到 AccessToken,这个往往能用比较长的一段时间;
要么更新至新版 API 的登录接口用密码拿到 AccessToken;有两个途径:抓包,得到新版接口的结构;逆向 App。
要么曲线救国,直接利用已有的 AccessToken(如果失效就人工更新);一般是把登录后得到的 Token 保存在 App 数据库里面,Root 后拿到应该是不难的。
要么模拟微信公众号的认证过程获得授权(这个可以绕过易班主 App 的认证);个人认为这个最难,成功概率最低。
尝试 1:利用主站 PC 网页端登录,用从网页端 ajaxlogin 接口拿到的 token 来尝试;结果:失败,似乎移动端网页端的认证是分开的。从产品的角度感觉也是分开的,PC 网页与移动端很多功能都不交叉。
尝试 2:在网上找到一些 其他的资料,将 v4 接口提示缺的 ct 等字段补充了;结果:最后接口就直接报客户端版本低,似乎与 ct 与 identity 的值有关。
尝试 3:么的办法,还是只能抓包试一下了,看看到底是个啥接口。
由于 Android 7 以上系统对 SSL 证书进行了二次校验,App 默认将不再信任用户加载的证书(除非 Root 后加载到根证书区域),为了避免麻烦,我尝试使用 Android 6 的模拟器,将模拟器的网络流量导入 Fiddler。第一次使用的模拟器是 x86 的,无法安装直接报不符合 ABI 错误,看来 App 没有声明 X86 的支持。
PS C:\Users\fyyo4\AppData\Local\Android\Sdk\platform-tools> .\adb install C:\Users\fyyo4\Downloads\yiban_20210810_501.apk
Performing Push Install
C:\Users\fyyo4\Downloads\yiban_20210810_501.apk: 1 file pushed, 0 skipped. 230.0 MB/s (31692716 bytes in 0.131s)
pkg: /data/local/tmp/yiban_20210810_501.apk
Failure [INSTALL_FAILED_NO_MATCHING_ABIS]
后面换了个 armel 的镜像,不知道是因为存在指令集转换本来就这么慢还是 Android AVD 的官方模拟器跨指令集效率太低,模拟器十分缓慢,在我的电脑上完全无法正常使用。
后续可以更换其他模拟器尝试一下,但是就有点繁琐,先放在这里吧。
为什么不用实机?实体机是 Android 9 的,要关闭 HTTPS Pinning 要么 Xposed HOOK(比如插件 JustTrustMe),要么想办法修改 Target API 低于 Android 7。
一般的做法是基于后者,用平行空间等双开 App 动态运行 APK,如果宿主 APP(平行空间)的 Target API 是低于 Android 7 的,那么运行在宿主 App 容器中的目标 App 就变相降低了 Target API,这个时候抓宿主 App 的包就都可以抓到了。但是在我的手机上总是安装不上平行空间的 64 位支持包。
前者也有尝试过太极和 VirtualXposed。太极可以正常使用,但是太极中 JustTrustMe 似乎没有启用,App 还是报错 Trust anchor for certification path not found. VirtualXposed 则无法正常安装易班,提示不支持 32 位应用。更换最后一个支持 32 位应用的版本(0.18.2)后,可以正常安装了,但是无法运行容器中的易班 App。
iOS 抓包呢?iOS 似乎没有那么严格的证书问题,正常载入的证书似乎都会信任。但是这该死的 App 还对代理做了检测,我们暂时没有什么太好的办法接管网络流量。要么换区付费装一个能以 VPN 形式接管网络数据的 App 然后想办法将流量送到 Fiddler 的 HTTP 代理做处理,要么弄一个特殊处理过的路由器单独给手机用,在网关将数据转发到 HTTP 代理处理。但无论是哪个都是不小的工程量,很繁琐,暂时先搁置在这里。
尝试 4:想办法逆向 App。拉进 JEB,看了一下,有很明显的加壳特征,确定是腾讯御安全加固。
查了一下网上的资料,静态脱壳似乎都是一些商业化的成品工具,比如 “MT 管理器”“ARM Pro” 什么的,没找到一个通用一点的方法;动态脱壳在手机上可以使用 “fdex”“反射大师” 什么的,PC 上的可以用 IDA。动态脱壳原理基本都是等壳加载完了准备真正注入内存运行时,在加载至内存这个关键点做文章,有点 X86 脱压缩壳的味道。这几天查资料的过程中有个感受,就是 Android 的壳好像一般对源程序不方便做太多处理,所以攻防点基本上都是不让你 Debug,不让你拿到真实的 dex,一旦拿到真实 dex 基本就是胜利了。
现有条件不方便脱壳,还是只能去装虚拟机了,当然装了虚拟机会先试试抓包的。
简单的尝试脱壳
更新于 2022/2/20.
想办法搞出来了一个 Xposed 环境。尝试了 Fdex2 和反射大师,fdex2 加载后闪退;反射大师脱壳成功。
这里提供一下反射大师脱出来的 dex,源自易班 5.0.8,注意是不完整的,后文有提到。classes.dex
将 dex 再导入 jeb,可以看到确实得到了不少的内容。
在 com.yiban.app.login.utils
下有一个我们感兴趣的类:LoginUtil
,打开发现一部分登录相关的逻辑。可以看到,登录接口的参数,多了不止一点点哦。之前我们疑惑的那个 ct,没想到就是个常数😅。
我们关心的加密算法,也露出尾巴了,是从 com.yiban.app.helper.EncryptionHelper
这个类的 encryptPassword
这个方法加密来的。双击进入……嗯?怎么没反应,jeb 出 bug 了。再一看,我去,怎么根本就没有 com.yiban.app.helper 这个 package?
再细看,原来远不止这一个类没有,比如他用的 retrofit2 框架,整个框架都没有见着。所以,很明显我们得到的 dex 并不完整。尝试了一些其他的方法,也没有得到更完整的 dex,似乎陷入了僵局。
网上有说一些可能的原因,就是壳在 dex 的相关位置先 nop 掉,等到程序真正加载时再填充;似乎也不是我这种情况,我的 dex 并没有出现 nop,而且我是运行过相关逻辑后再脱壳的;我的脱壳环境是 Android 5.0.1,也没有高版本系统那些乱七八糟的优化带来的问题。
顺带地,在建立 Xposed 环境时,因为是老的安卓版本,抓包也没有问题了。用 HttpCanery 直接就可以成功抓包,App 并没有单独验证证书。根据抓包的情况看,后面的接口应该没有怎么改动。但是我拿 accesskey 直接跑脚本,因为一些其他的原因脚本也失败了,不确定是不是个例。