利昂图书馆预约App分析(2020年9月3日)

受学长启发,打算去研究下学校图书馆的预约 App 实现自习室自动抢座,以下是我的研究过程。

Disclaimer

仅供学习使用,请勿用于非法用途,也请尊重早起抢座的同学的利益。

从 Google 开始

先去 Google 了下,发现好多利昂图书馆预约脚本,但看相关的 issue,好像在去年五月(那时候我还在摸高考)利昂图书馆预约软件升级后而无法使用。

抓包分析

这里使用了Packet Capture(Google Play),发现其启用了 https,装 CA。

发现其存在以下有趣的内容:

  • x-hmac-request-key
  • x-request-date
  • x-request-id

由于自己之前曾经尝试 implement 过 yubico 的 OTP 插件,对 hmac signing 略有了解,HMAC signing 需要 message 与 key,而 message 一般是 http request 的 body,而且往往伴随着 nonce,key 一般预分发的,应该会被硬编码进 app 中。而 x-hmac-request-key 是 64 字节,应该是 HMAC-SHA256

这里并没有看到 nonce,所以就把 x-request-id 看作 nonce,接下来的目标就是去逆向 app。

app 的逆向(寻 Key 大挑战)

逐渐发现是使用 Flutter AOT

这里使用了jadx-gui,在 source code 中翻了大半天还是没找到任何有关 http 请求或 hmac 算法相关的代码,感觉应该是编译成了.so文件。

这个.so文件一看不得了,只有一个libflutter.so,而且 resources\assets 目录下还有以下文件。

  • isolate_snapshot_data
  • isolate_snapshot_instr
  • vm_snapshot_data
  • vm_snapshot_instr

linux 下 file 下?data!??

Google 了下才知道是用 flutter 完成的开发,而且启用了 flutter AOT。再查 flutter 逆向相关,并没有任何工具,而且还真的是劝退了不少人。

我使用 binary ninja demo 把每个文件都试了个遍,并无法识别平台。

确定努力方向为 Snapshot

通过官方文档Flutter-engine-operation-in-AOT-Mode可知其使用了 Dart VM。突然感觉难度上升了超多。Jesus…

不过一个好消息是文档中提到其中的Snapshot保存了heap的初始状态,那么应该可以拿到 key。我花了一晚上去找 Key 并没有任何成果。第二天有课先睡觉。

第二天下午没课打开Cutter继续寻找线索。在 Snapshot 中我发现x-request-idx-request-key这两个字符串是在一起的,于是我猜测 Key 应该也是与 HMAC 算法相关的在一起,通过我不断努力搜索,终于找到了一个令我感兴趣的字符串leos3cr3t,先将其看作 key。

一点有趣的东西

在寻 Key 过程中我也发现了 x-request-id 的生成方式为UUIDv1

寻找 Message 组成以及验证 Key 是否正确

从原本的经验出发

依照自己之前 implement yubico OTP 的经验,message 应该是 request body。于是我尝试对其进行各种变换(包括加入 x-request-id),发现 HMAC-SHA256 的结果总是不对,那么应该还有其他的东西。

尝试确定有哪些项目用于 HMAC-SHA256 的生成

做到后面我开始有些找不到着手点,打算推翻自己之前靠经验做出的判断。从头开始确定到底哪些东西会用于 HMAC-SHA256 的 message 部分,这里使用了 Postman 发送请求。

让我比较傻眼的是,我尝试改变 request params,服务器并没有返回非法访问错误,那么 body 应该与其无关。

那么 API endpoint、server url 与其有关吗?我也尝试访问奇怪的路径、使用 IP 访问,发现返回信息也正常,那么与这些因素也无关。

那么只有x-request-datex-request-id了,我尝试改变这两个参数,都会触发非法访问错误,那么这两个参数应该是与 HMAC-SHA256 的 message/key 有关了。

但我无论怎么改变这两个参数的排列组合,HMAC-SHA256 出来的结果还是有问题,要么 Key 不对、要么是还有其他信息,但我相信 Key 是正确的。

换一个思路从内存入手

我更相信 message 中还有更多的信息,所以我打算直接去 dump 这个 app 的运行时内存,既然x-request-id是其 message 中的一员,那么在不做变形的情况下应该是可以在内存中追踪到完整变化的字符串的。

并没找到比较好用的 Android 设备中 dump 内存的工具,因此使用了Game Guardian,同时启用抓包工具对发送的请求进行记录。

最终成功 dump 出约 800M 的文件,先检验下 Key(暂定)是否存在于内存,这里使用了个简单的 bash 循环+strings+grep 来快速找出相关的信息。

1
2
3
4
for i in cn.com.libseatapp-*
do
strings $i | grep leos3cr3t
done

看起来蛮不错的,让我充满了信心。

再去看下能不能拿出对应的 UUID 变换过程。

漂亮!

那么去验证下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import hmac
import hashlib

time = '1599126887803'
UUID = '807cd4b0-e010-11ea-b4b5-11d182874e0f'
secret = 'leos3cr3t'
expected = 'adf116c5a2b46fbf0753022213e0d25050b6c5620995c71a61d98abe9f18bf13'

message = "seat::{}::{}::GET".format(UUID, time)

signature = hmac.new(bytes(secret, 'utf-8'), msg=bytes(message, 'utf-8'), digestmod=hashlib.sha256).hexdigest()

print('Message: {}'.format(message))
print('Expected: {}'.format(expected))
print('Result: {}'.format(signature))

if signature == expected:
print('Match')

CHEERS!

总结

利昂客户端验证相关

根据上文得出的 message 组成,大概可以推断其组成如下:
seat::<UUIDv1>::<Timestamp>::<Request Method>
Key 为leos3cr3t
而且在测试过程中我发现他会对时间差进行验证,超时即使正确的签名也会被返回非法访问。
Anyway,message 的组成与 key 都弄出来了,也不怕什么了。

个人感受

Flutter AOT 是真心难折腾,如果可以的话真的不想再碰一次了。总之也是学到不少东西的。