自动化易班健康打卡

疫情又反复了,学校又开始要求打卡了。可最近的阴间作息(3点睡12点起)让我实在难以完成学校的要求:每天12点前上报健康状况。所以只能寻求自动化手段了。

在网上稍微搜寻,发现已有前辈做的研究了,Github仓库。但是原作者已删库跑路,易班的API似乎也有更新,打卡的表单也有修改,所以我们还需要稍作修改。

1.png

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包,分析打卡过程如下:

  1. 获得未完成的任务列表,每个任务有一个TaskID

  2. 根据TaskID拿到表单模板ID(WFid)

  3. 请求拿到表单模板的各个字段名称,ID,类型等等。(模拟提交的程序不需要这个接口)

  4. 将表单的数据与对应的任务ID POST提交回去。

2.png
3.png
4.png
5.png

与前辈的代码对比,发现表单提交的部分并没有改动,相应的修改了表单结构替换代码即可:

dict_form = {"2fca911d0600717cc5c2f57fc3702787": ["湖南省", "123", "456"],
"db19a5f588037889a2ff5b6894a303a9":"绿码",
"19ba06c51ab8eee843a235ece7064865":"第二针",
"2d4135d558f849e18a5dcc87b884cce5": "36", # 随机体温
"c77d35b16fb22ec70a1f33c315141dbb": util.get_time_no_second()}

2.接口变更

改好后试了一下,发现报登录失败,但是检查了账号信息是没有错误的。

6.png

查看前辈代码中登录相关的部分,发现是登录的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”,而该字段的含义就不清楚了,所以在这里我想着在易班的产品上再找找机会。

7.png

整理一下思路:

我们的目的是拿到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代理处理。但无论是哪个都是不小的工程量,很繁琐,暂时先搁置在这里。

8.png

尝试4:想办法逆向App。拉进JEB,看了一下,有很明显的加壳特征,确定是腾讯御安全加固。

9.png

查了一下网上的资料,静态脱壳似乎都是一些商业化的成品工具,比如“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,可以看到确实得到了不少的内容。

Snipaste_2022-02-20_00-48-55.png

在com.yiban.app.login.utils下有一个我们感兴趣的类:LoginUtil,打开发现一部分登录相关的逻辑。可以看到,登录接口的参数,多了不止一点点哦。之前我们疑惑的那个ct,没想到就是个常数😅。

Snipaste_2022-02-20_00-50-17.png

我们关心的加密算法,也露出尾巴了,是从com.yiban.app.helper.EncryptionHelper这个类的encryptPassword这个方法加密来的。双击进入……嗯?怎么没反应,jeb出bug了。再一看,我去,怎么根本就没有com.yiban.app.helper这个package?

Snipaste_2022-02-20_01-00-59.png

再细看,原来远不止这一个类没有,比如他用的retrofit2框架,整个框架都没有见着。所以,很明显我们得到的dex并不完整。尝试了一些其他的方法,也没有得到更完整的dex,似乎陷入了僵局。

网上有说一些可能的原因,就是壳在dex的相关位置先nop掉,等到程序真正加载时再填充;似乎也不是我这种情况,我的dex并没有出现nop,而且我是运行过相关逻辑后再脱壳的;我的脱壳环境是Android 5.0.1,也没有高版本系统那些乱七八糟的优化带来的问题。

顺带地,在建立Xposed环境时,因为是老的安卓版本,抓包也没有问题了。用HttpCanery直接就可以成功抓包,App并没有单独验证证书。根据抓包的情况看,后面的接口应该没有怎么改动。但是我拿accesskey直接跑脚本,因为一些其他的原因脚本也失败了,不确定是不是个例。