lamda 3.155__tar.gz → 5.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. {lamda-3.155 → lamda-5.0}/PKG-INFO +2 -2
  2. {lamda-3.155 → lamda-5.0}/README.md +212 -110
  3. {lamda-3.155 → lamda-5.0}/lamda/__init__.py +1 -1
  4. {lamda-3.155 → lamda-5.0}/lamda/client.py +215 -38
  5. {lamda-3.155 → lamda-5.0}/lamda/exceptions.py +3 -1
  6. {lamda-3.155 → lamda-5.0}/lamda/rpc/services.proto +17 -1
  7. lamda-5.0/lamda/rpc/storage.proto +13 -0
  8. {lamda-3.155 → lamda-5.0}/lamda.egg-info/PKG-INFO +2 -2
  9. {lamda-3.155 → lamda-5.0}/lamda.egg-info/SOURCES.txt +1 -0
  10. {lamda-3.155 → lamda-5.0}/lamda.egg-info/requires.txt +5 -2
  11. {lamda-3.155 → lamda-5.0}/setup.py +6 -3
  12. {lamda-3.155 → lamda-5.0}/lamda/bcast.proto +0 -0
  13. {lamda-3.155 → lamda-5.0}/lamda/const.py +0 -0
  14. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/any.proto +0 -0
  15. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/api.proto +0 -0
  16. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/compiler/plugin.proto +0 -0
  17. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/descriptor.proto +0 -0
  18. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/duration.proto +0 -0
  19. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/empty.proto +0 -0
  20. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/field_mask.proto +0 -0
  21. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/source_context.proto +0 -0
  22. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/struct.proto +0 -0
  23. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/timestamp.proto +0 -0
  24. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/type.proto +0 -0
  25. {lamda-3.155 → lamda-5.0}/lamda/google/protobuf/wrappers.proto +0 -0
  26. {lamda-3.155 → lamda-5.0}/lamda/rpc/application.proto +0 -0
  27. {lamda-3.155 → lamda-5.0}/lamda/rpc/debug.proto +0 -0
  28. {lamda-3.155 → lamda-5.0}/lamda/rpc/file.proto +0 -0
  29. {lamda-3.155 → lamda-5.0}/lamda/rpc/policy.proto +0 -0
  30. {lamda-3.155 → lamda-5.0}/lamda/rpc/proxy.proto +0 -0
  31. {lamda-3.155 → lamda-5.0}/lamda/rpc/settings.proto +0 -0
  32. {lamda-3.155 → lamda-5.0}/lamda/rpc/shell.proto +0 -0
  33. {lamda-3.155 → lamda-5.0}/lamda/rpc/status.proto +0 -0
  34. {lamda-3.155 → lamda-5.0}/lamda/rpc/types.proto +0 -0
  35. {lamda-3.155 → lamda-5.0}/lamda/rpc/uiautomator.proto +0 -0
  36. {lamda-3.155 → lamda-5.0}/lamda/rpc/util.proto +0 -0
  37. {lamda-3.155 → lamda-5.0}/lamda/rpc/wifi.proto +0 -0
  38. {lamda-3.155 → lamda-5.0}/lamda/types.py +0 -0
  39. {lamda-3.155 → lamda-5.0}/lamda.egg-info/dependency_links.txt +0 -0
  40. {lamda-3.155 → lamda-5.0}/lamda.egg-info/not-zip-safe +0 -0
  41. {lamda-3.155 → lamda-5.0}/lamda.egg-info/top_level.txt +0 -0
  42. {lamda-3.155 → lamda-5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lamda
3
- Version: 3.155
3
+ Version: 5.0
4
4
  Summary: Android reverse engineering & automation framework
5
5
  Home-page: https://github.com/rev1si0n/lamda
6
6
  Author: rev1si0n
@@ -12,6 +12,6 @@ Classifier: Intended Audience :: Science/Research
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Operating System :: Android
14
14
  Classifier: Topic :: Security
15
- Requires-Python: >=3.6,<3.11
15
+ Requires-Python: >=3.6,<3.12
16
16
  Provides-Extra: next
17
17
  Provides-Extra: full
@@ -8,6 +8,7 @@ LAMDA 是一个用于逆向及自动化的辅助框架,它设计为减少安
8
8
 
9
9
  * 零依赖,只需 **root** 即可
10
10
  * 前身通过超500台设备压力的稳定生产环境考验
11
+ * 可通过扩展模块使用完整的安卓内 Debian (12, bookworm) 环境
11
12
  * 通过接口轻松设置根证书,配合 http/socks5 代理实现中间人
12
13
  * 通过 frida 暴露内部 Java 接口(类 [virjar/sekiro](https://github.com/virjar/sekiro) 但基于 frida)
13
14
  * 近乎商业级软件的质量和稳定性,ARM/X86全架构
@@ -34,6 +35,7 @@ LAMDA 是一个用于逆向及自动化的辅助框架,它设计为减少安
34
35
  * 可使用 ssh 登录设备终端
35
36
  * 只要有网即可连接任意地方运行了 LAMDA 的设备
36
37
  * 前后台运行 shell 命令,授予撤销应用权限等
38
+ * 内置 Storage 用于存储设备变量
37
39
  * 内置 http/socks5 代理,可设置系统/指定应用的代理
38
40
  * 内置 frida 15.x, IDA 7.5 server 等工具
39
41
  * 内置 crontab 定时任务
@@ -43,7 +45,10 @@ LAMDA 是一个用于逆向及自动化的辅助框架,它设计为减少安
43
45
  * WEB 端文件上传下载
44
46
  * UI自动化,通过接口实现自动化操作
45
47
 
46
- 如果觉得以下教程过于复杂看不懂,可以选择观看网友制作的 [视频教程](https://lamda.run/tutorial/video)。
48
+ 如果觉得以下教程过于复杂看不懂,可以选择观看 [视频教程](https://lamda.run/tutorial/video)。
49
+
50
+
51
+ ![动图演示](image/demo.gif)
47
52
 
48
53
  ## 无视恶意软件对抗
49
54
 
@@ -53,7 +58,7 @@ MOMO (vvb2060) 是我们认为目前最强的ROOT特征检测软件,如 MOMO
53
58
 
54
59
  ## 一键中间人流量分析
55
60
 
56
- 支持常规以及国际APP流量分析,DNS流量分析,得益于 [mitmproxy flow hook](https://docs.mitmproxy.org/stable/api/events.html),你可以对任何请求做到最大限度的掌控,mitmproxy 功能足够丰富,你可以通过其 `Export` 选项导出特定请求的 `curl` 命令或者 `HTTPie` 命令,分析重放、拦截修改、功能组合足以替代你用过的任何此类商业/非商业软件。如果你仍不清楚 mitmproxy 是什么以及其具有的能力,请务必先查找相关文档,因为 LAMDA 将会使用 mitmproxy 为你展现应用请求。
61
+ 支持常规以及国际APP流量分析,DNS流量分析,得益于 [mitmproxy flow hook](https://docs.mitmproxy.org/stable/api/events.html),你可以对任何请求做到最大限度的掌控,mitmproxy 功能足够丰富,你可以使用 Python 脚本实时修改或者捕获应用的请求,也可以通过其 `Export` 选项导出特定请求的 `curl` 命令或者 `HTTPie` 命令,分析重放、拦截修改、功能组合足以替代你用过的任何此类商业/非商业软件。如果你仍不清楚 mitmproxy 是什么以及其具有的能力,请务必先查找相关文档,因为 LAMDA 将会使用 mitmproxy 为你展现应用请求。
57
62
 
58
63
  通过 tools/ 目录下的 `globalmitm`,`startmitm.py` 实现,使用方法请看其同目录 README。
59
64
 
@@ -61,7 +66,7 @@ MOMO (vvb2060) 是我们认为目前最强的ROOT特征检测软件,如 MOMO
61
66
 
62
67
  ## 拖拽上传
63
68
 
64
- 可直接在远程桌面拖拽上传,支持上传整个目录,最大支持单个 256MB 的文件,文件将始终被上传到 `/data/local/tmp` 目录下。
69
+ 可直接在远程桌面拖拽上传,支持上传整个目录,最大支持单个 256MB 的文件,文件将始终被上传到 `/data/usr/uploads` 目录下。
65
70
 
66
71
  ![拖拽上传动图演示](image/upload.gif)
67
72
 
@@ -92,8 +97,6 @@ MOMO (vvb2060) 是我们认为目前最强的ROOT特征检测软件,如 MOMO
92
97
 
93
98
  如果你希望继续看下去,请确保:有一台已经 root 且运行内存大于 2GB,可用存储空间大于 1GB 的安卓设备或者安卓模拟器(推荐使用最新版**夜神**,**雷电**模拟器,或者 AVD [Android Studio Virtual Device])。**不完全支持** 网易 Mumu,**不支持**腾讯手游助手、蓝叠以及安卓内虚拟如 VMOS 等),对于真机,推荐运行最接近原生系统的设备如谷歌系、一加、安卓开发板等,或系统仅经过轻度改造的设备。如果你使用的是OPPO/VIVO/华为/小米的设备,经过尝试后无法正常运行,建议改用模拟器。
94
99
 
95
- 对于**云手机**,支持阿里云/华为云手机,不支持X手指、X多云、X电、X云兔、X子星、X马云及任何其他品牌。
96
-
97
100
  <br>
98
101
 
99
102
  # 目录
@@ -111,8 +114,6 @@ MOMO (vvb2060) 是我们认为目前最强的ROOT特征检测软件,如 MOMO
111
114
  - [安装服务端](#安装服务端)
112
115
  - [通过 Magisk 安装](#通过-magisk-安装)
113
116
  - [手动安装](#手动安装)
114
- - [方式 1](#方式-1)
115
- - [方式 2](#方式-2)
116
117
  - [启动服务端](#启动服务端)
117
118
  - [退出服务端](#退出服务端)
118
119
  - [卸载服务端](#卸载服务端)
@@ -131,6 +132,7 @@ MOMO (vvb2060) 是我们认为目前最强的ROOT特征检测软件,如 MOMO
131
132
  - [使用 FRIDA 暴露 Java 接口](#使用-frida-暴露-java-接口)
132
133
  - [使用内置的定时任务](#使用内置的定时任务)
133
134
  - [使 LAMDA 可被任意地点连接](#使-lamda-可被任意地点连接)
135
+ - [读写内置键值存储器](#读写内置键值存储器)
134
136
  - [读写系统属性](#读写系统属性)
135
137
  - [读写系统设置](#读写系统设置)
136
138
  - [获取设备运行状态](#获取设备运行状态)
@@ -146,6 +148,7 @@ MOMO (vvb2060) 是我们认为目前最强的ROOT特征检测软件,如 MOMO
146
148
  - [进阶UI操作](#进阶ui操作)
147
149
  - [接口锁](#接口锁)
148
150
  - [使用内置终端](#使用内置终端)
151
+ - [使用 Debian 环境扩展模块](#使用-debian-环境扩展模块)
149
152
  - [工具及教程](#工具及教程)
150
153
  - [一键中间人](#一键中间人)
151
154
  - [国际代理进行中间人](#国际代理进行中间人)
@@ -159,15 +162,13 @@ MOMO (vvb2060) 是我们认为目前最强的ROOT特征检测软件,如 MOMO
159
162
 
160
163
  # 免责声明及条款
161
164
 
162
- 为了下载使用由 rev1si0n (账号 github.com/rev1si0n)(以下简称“本人”)个人开发的软件 LAMDA ,您应当阅读并遵守《用户使用协议》(以下简称“本协议”)。请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受;除非您已阅读并接受本协议所有条款,否则您将无权下载、安装或使用本软件及相关服务。您的下载、安装、使用、获取账号、登录等行为即视为您已阅读并同意受到上述协议的约束;若您需要获得本服务,您(以下称"用户")应当同意本协议的全部条款并按照页面上的提示完成全部申请使用程序。您可以在本文档的相同目录找到 [DISCLAIMER.TXT](DISCLAIMER.TXT),或者点此 [免责声明](DISCLAIMER.TXT) 查阅。由于并未完全开源,除以上条款外:**授权您对 LAMDA 本身进行以恶意代码分析为目的的逆向**。
165
+ 为了下载使用由 rev1si0n (账号 github.com/rev1si0n)(以下简称“本人”)个人开发的软件 LAMDA ,您应当阅读并遵守《用户使用协议》(以下简称“本协议”)。请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款,并选择接受或不接受;除非您已阅读并接受本协议所有条款,否则您将无权下载、安装或使用本软件及相关服务。您的下载、安装、使用、获取账号、登录等行为即视为您已阅读并同意受到上述协议的约束;若您需要获得本服务,您(以下称"用户")应当同意本协议的全部条款并按照页面上的提示完成全部申请使用程序。您可以在本文档的相同目录找到 [DISCLAIMER.TXT](DISCLAIMER.TXT),或者点此 [免责声明](DISCLAIMER.TXT) 查阅。此项目代码库中仅包含开源的客户端库、工具代码。因服务端程序以二进制方式发布,并未开源,所以除以上条款外:**授权您对 LAMDA SERVER 本身进行以恶意代码分析为目的的逆向**。
163
166
 
164
167
  请确认您已阅读并接受本协议所有条款,否则您将无权下载、安装或使用本软件及相关服务。
165
168
 
166
169
  # 前言
167
170
 
168
- LAMDA 是个人开发的免费软件 (freeware),目前仅客户端及协议是开源的,但个人承诺它没有任何对您违规或多余的行为,如果仍有担心,您可以**立即离开**或者选择**付费**寻求心理安慰。互相尊重,使用请遵守使用条款。合作交流可以 [点此发送邮件](mailto:ihaven0emmail@gmail.com)。
169
-
170
- 为什么部分开源?因为 LAMDA 亦黑亦白,很容易被不法分子利用使作者处于危险之中,所以请尊重条款使用。建议在 Linux 或者 Mac 系统上操作文档及样例中的代码。部分功能需要配合 `tools/` 目录下的工具实现,如何使用请参照 [tools/README.md](tools/README.md)。
171
+ LAMDA 是个人开发的免费软件 (freeware),目前仅客户端库及工具代码是开源的,个人承诺 LAMDA 不会对您及您的设备有任何违规或多余的行为,如果仍有担心,您可以**立即离开**或者选择**付费**寻求心理安慰。互相尊重,使用请遵守使用条款。为什么部分开源?因为 LAMDA 亦黑亦白,很容易被不法分子利用使作者或者不明所以的用户处于危险之中,所以请尊重条款使用。建议在 Linux 或者 Mac 系统上操作文档及样例中的代码。部分功能需要配合 `tools/` 目录下的工具实现,如何使用请参照 [tools/README.md](tools/README.md)。
171
172
 
172
173
  **特别注意**:**请勿在自用设备上运行,当有可能在公网或不信任的网络中使用时,务必确保在启动时指定了PEM证书**
173
174
 
@@ -180,7 +181,7 @@ LAMDA 是个人开发的免费软件 (freeware),目前仅客户端及协议是
180
181
  因为安卓被各种设备广泛使用,无法保证百分百的兼容性,可能会有运行异常等各种未知情况,出现的异常情况包括:无故重启,APP经常崩溃,触摸失效或无故乱动等等,冻屏等情况。如果经常遇到,建议停止使用。
181
182
  点此 [报告问题/建议](https://github.com/rev1si0n/lamda/issues/new),请详细描述并附上机型系统等信息。
182
183
 
183
- 社区讨论:[电报 t.me/lamda_dev](https://t.me/lamda_dev) | [gitter.im/lamda-dev](https://gitter.im/lamda-dev/community)
184
+ 社区讨论:[电报 t.me/lamda_dev](https://t.me/lamda_dev)
184
185
 
185
186
  > 顺便支持作者
186
187
 
@@ -204,7 +205,6 @@ LAMDA 最理想的运行环境是你刚刚 root(如:新建模拟器,自带
204
205
  ```
205
206
  * 必须关闭 Magisk Hide
206
207
  * 必须关闭 frida-server
207
- * 建议关闭 Xposed/Magisk 插件
208
208
  * 确认完毕重启设备
209
209
  ```
210
210
 
@@ -292,7 +292,7 @@ docker run -itd --rm --privileged --pull always -v /lib/modules:/lib/modules:ro
292
292
 
293
293
  ## 安装客户端
294
294
 
295
- 请使用 3.6 - 3.10 版本的 Python,建议有条件使用 Python 3.9
295
+ 请使用 3.6 - 3.11 版本的 Python,建议有条件使用 Python 3.9
296
296
 
297
297
  ```bash
298
298
  pip3 install -U lamda
@@ -302,6 +302,8 @@ pip3 install -U lamda
302
302
  # 你可能需要外网访问来安装 frida,否则可能会卡住许久(~10分钟)直至安装失败
303
303
  # 即使之前安装过 frida,也应该重新执行以下命令
304
304
  pip3 install -U --force-reinstall 'lamda[full]'
305
+ # 如果你安装的服务端是 7.0 (beta) 版本,请执行如下命令安装
306
+ pip3 install -U --force-reinstall 'lamda[next]'
305
307
  # 请注意完成安装后,你需要同时使用 pip 更新任何依赖 frida
306
308
  # 的第三方库例如 frida-tools objection 等(如果安装过的话)
307
309
  # 否则后期使用可能会出现难以察觉的异常
@@ -325,7 +327,9 @@ pip3 install -U --force-reinstall lamda
325
327
 
326
328
  ## 安装服务端
327
329
 
328
- **默认方式安装的 LAMDA 没有开启任何认证,其他人可以访问你设备上的任意内容,监听你的设备甚至接入你的网络进行进一步控制。请特别留意`启用接口认证`的部分,请务必在可以`信任的网络`内使用。**
330
+ **默认方式安装的 LAMDA 没有开启任何认证,其他人可以访问设备上的任意内容,监听你的设备甚至接入设备网络进行进一步控制。请特别留意`启用接口认证`的部分,请务必在可以`信任的网络`内使用。并且请注意,`即使开启了接口认证`,任何`有权限登录远程桌面以及使用API`的人仍然对你的设备以及 LAMDA 本身有着完全的访问权限。**
331
+
332
+ **由于安全性原因,我们不建议将任何相关文件放在 `/data/local/*` 目录下。**
329
333
 
330
334
  安装前,请先选择合适的架构,可以通过 adb shell 命令 `getprop ro.product.cpu.abi` 来获取当前的系统架构。
331
335
  正常情况下,对于现时代的手机,可以直接选择 `arm64-v8a` 版本,而对于模拟器如雷电,你会在新建模拟器时选择32或64位版本的安卓系统,
@@ -334,8 +338,8 @@ pip3 install -U --force-reinstall lamda
334
338
  LAMDA 支持设备状态主动上报,你可以编写接口或使用 grafana 来记录设备运行状况,其中包含了系统、网络、内存、CPU、磁盘等等信息。
335
339
 
336
340
  ```bash
337
- # 如果不清楚这个功能是什么请不要执行,注意替换掉以下链接
338
- echo "stat-report.url=http://example.com/report" >>/data/local/tmp/properties.local
341
+ # 如果不清楚这个功能是什么请不要执行,注意替换掉以下链接(需要 root 身份)
342
+ echo "stat-report.url=http://example.com/report" >>/data/properties.local
339
343
  ```
340
344
 
341
345
  这样 LAMDA 会在启动后**每分钟**向此链接**POST**设备状态信息(JSON),由于字段较多,将不在此罗列。
@@ -347,8 +351,8 @@ LAMDA 存在一个自动更新的逻辑,但是由于存在分钟级的服务
347
351
  如果你确实不在意更新时分钟级的服务不可用,启动 LAMDA 之前写入以下配置文件可以确保 LAMDA 始终为最新版本。
348
352
 
349
353
  ```bash
350
- # 进入 adb shell 执行
351
- echo "upgrade.channel=latest" >>/data/local/tmp/properties.local
354
+ # 进入 adb shell 执行(需要 root 身份)
355
+ echo "upgrade.channel=latest" >>/data/properties.local
352
356
  ```
353
357
 
354
358
  > properties.local 启动配置
@@ -356,7 +360,7 @@ echo "upgrade.channel=latest" >>/data/local/tmp/properties.local
356
360
  在开始前,有必要介绍一下上面的 `properties.local` 文件,
357
361
  properties.local 为 LAMDA 的启动配置文件,通常存储于设备之上,其中包含了 `a=b` 类型的字符串,
358
362
  通过编写此文件,你可以实现在 LAMDA 启动时自动连接到 OpenVPN、代理、端口转发等。
359
- LAMDA 在启动时,会从 `/data/usr`、`/data/local/tmp`、`${CFGDIR:-/data/local}` 查找该文件并载入。
363
+ LAMDA 在启动时,会从 `/data`, `/data/usr` 查找该文件并载入(usr 目录在 LAMDA 首次启动前并不存在,所以你可能需要手动创建)。
360
364
  你可以在以上三个位置任意一个放置你的 properties.local 配置文件。
361
365
 
362
366
  除了 `properties.local`,还有一个从加载远端配置的参数 `--properties.remote`,它可以让 LAMDA 在启动时从HTTP服务器下载配置,请继续看往启动 LAMDA 的章节。
@@ -387,77 +391,57 @@ abi not match (使用了错误的 tar.gz 包)
387
391
 
388
392
  ### 手动安装
389
393
 
390
- 手动安装是通常做法,下面将会介绍两种方式,两种方式的区别是:部分老旧设备可能无法通过系统的 `tar` 命令来解压 tar.gz 后缀的文件,所以提供了 `*-install.sh` 用来作为补充,其内置了一个 busybox 用来解压。已知 getprop 获得的设备架构为 `arm64-v8a`,现在将设备连接到当前电脑并确保已授权 ADB、可以正常切换 root。
391
-
392
- #### 方式 1
394
+ 由于部分老旧设备可能无法通过系统的 `tar` 命令来解压 tar.gz 后缀的文件,所以提供了 `busybox` 用来作为补充,你可能需要同时下载提供的 busybox。现在已知 getprop 获得的设备架构为 `arm64-v8a`,现在将设备连接到当前电脑并确保已授权 ADB、可以正常切换 root。
393
395
 
394
396
  从 `release` 页面 [lamda/releases](https://github.com/rev1si0n/lamda/releases)
395
- 下载 `arm64-v8a.tar.gz-install.sh`。
397
+ 下载 `lamda-server-arm64-v8a.tar.gz` 以及 `busybox-arm64-v8a`。
396
398
 
397
399
  ```bash
398
- # /data/local/tmp 是标准服务安装路径,但并不是强制要求
399
- # 你可以放到除了 /sdcard 之外任何具备可读写权限的文件夹
400
- adb push arm64-v8a.tar.gz-install.sh /data/local/tmp
401
- # 进入 adb shell
402
- adb shell
403
- # 输入 su 确保为 root 身份
404
- su
405
- # 切换到目录
406
- cd /data/local/tmp
407
- # 执行安装脚本并启动(这将解包并启动服务)
408
- sh arm64-v8a.tar.gz-install.sh
409
- # 删除安装包
410
- rm arm64-v8a.tar.gz-install.sh
411
- ```
412
-
413
- #### 方式 2
414
-
415
- 从 `release` 页面 [lamda/releases](https://github.com/rev1si0n/lamda/releases)
416
- 下载 `arm64-v8a.tar.gz`。
417
-
418
- ```bash
419
- # /data/local/tmp 是标准服务安装路径,但并不是强制要求
420
- # 你可以放到除了 /sdcard 之外任何具备可读写权限的文件夹
421
- adb push arm64-v8a.tar.gz /data/local/tmp
400
+ # 将文件临时推送到 /data/local/tmp
401
+ adb push lamda-server-arm64-v8a.tar.gz /data/local/tmp
402
+ adb push busybox-arm64-v8a /data/local/tmp
422
403
  ```
423
404
 
424
405
  完成后,进入 `adb shell`,解包文件:
425
406
 
426
407
  ```bash
427
408
  # 你现在应该在 adb shell 内
428
- cd /data/local/tmp
409
+ # 使用此种方式,服务端程序将被安装到 /data
410
+ # 确保切换为 root 身份
411
+ su
412
+ # 确保上传的 busybox 可执行
413
+ chmod 755 /data/local/tmp/busybox-arm64-v8a
414
+
415
+ cd /data
429
416
  # 解包服务端文件
430
- # 注意自带的 tar 命令可能因为不支持 z 选项导致解包失败,你可能需要使用 busybox tar 来解包
431
- # 如果系统不附带 busybox 命令,请自行从 https://busybox.net/downloads/binaries/1.20.0 下载合适架构的版本
432
- tar -xzf arm64-v8a.tar.gz
433
- # 删除安装包
434
- rm arm64-v8a.tar.gz
417
+ /data/local/tmp/busybox-arm64-v8a tar -xzf /data/local/tmp/lamda-server-arm64-v8a.tar.gz
418
+ # 服务将被解压到 /data/server 目录下
419
+
420
+ # 删除安装包以及 busybox
421
+ rm /data/local/tmp/lamda-server-arm64-v8a.tar.gz
422
+ rm /data/local/tmp/busybox-arm64-v8a
435
423
  ```
436
424
 
437
425
  ## 启动服务端
438
426
 
439
- 对于方式 1 安装,安装后会顺带启动服务,所以使用该方法**安装后**你无需执行下面的命令,但是按照下面的操作再来一次也并没有问题。
440
- 对于上面任意一种安装方法,你永远只需要在首次安装时操作,但是**启动服务**的过程则需要在每次 设备重启 或者 你手动关闭 LAMDA 后执行,因为 LAMDA 不会自己运行。
427
+ 使用 Magisk 安装后的 LAMDA 会在开机时自动启动,你只需要在首次安装后重启一次设备即可。而对于手动安装的 LAMDA,在每次**设备重启**或者**手动退出服务**后你都需要重新执行以下命令来启动 LAMDA SERVER。
441
428
 
442
429
  进入 adb shell,并切换为 `su` root 身份,执行:
443
430
 
444
431
  ```bash
445
- # 你现在应该在 adb shell 内,切换到目录
446
- cd /data/local/tmp
447
- #
432
+ # 确保为 root 身份
433
+ # 你现在应该在 adb shell 内
434
+ su
448
435
  # 启动服务端
449
- # 注意,arm64-v8a 这个目录根据平台不同名称也不同
450
- # 如果你使用的是 x86 版本,那么这个目录名则是 x86/,你需要对命令做相应修改
451
- sh arm64-v8a/bin/launch.sh
436
+ sh /data/server/bin/launch.sh
452
437
  #
453
438
  # 如果你想要启用加密传输
454
- # 请先使用 tools/ 中的 cert.sh/py 来生成PEM证书
455
- # 将其push到设备例如 /data/local/tmp/lamda.pem
439
+ # 请先使用 tools/ 中的 cert.py 来生成 PEM 证书
440
+ # 将其push到设备例如 /data/lamda.pem
456
441
  # 并将其属主及权限设置为 root 以及 600 (chown root:root lamda.pem; chmod 600 lamda.pem)
457
442
  # 并使用以下命令启动,lamda.pem 必须为绝对路径
458
- sh arm64-v8a/bin/launch.sh --certificate=/data/local/tmp/lamda.pem
459
- # 这将加密任何通过 LAMDA 客户端产生的通信流量
460
- # 但不包括 webui 远程桌面的流量
443
+ sh /data/server/bin/launch.sh --certificate=/data/lamda.pem
444
+ # 这将加密任何通过 LAMDA 产生的通信流量
461
445
  #
462
446
  # 从远端加载 properties.local
463
447
  # 有时候你可能希望从链接加载启动配置,这时你可以将 properties.local 上传到服务器
@@ -465,21 +449,21 @@ sh arm64-v8a/bin/launch.sh --certificate=/data/local/tmp/lamda.pem
465
449
  # 你也可以自行编写web服务来根据这些设备参数分发不同的启动配置
466
450
  # 建议使用 HTTPS 链接增加安全性,请确保设备时间正确。
467
451
  # 随后使用如下方式启动 LAMDA
468
- sh arm64-v8a/bin/launch.sh --properties.remote=http://example.com/config/properties.local
452
+ sh /data/server/bin/launch.sh --properties.remote=http://example.com/config/properties.local
469
453
  # 对于开启了 Basic Auth 的静态文件服务,同样支持提供用户名密码
470
- sh arm64-v8a/bin/launch.sh --properties.remote=http://user:password@example.com/config/properties.local
454
+ sh /data/server/bin/launch.sh --properties.remote=http://user:password@example.com/config/properties.local
471
455
  # 提示:LAMDA 会在超时或者返回 50x 状态码时重试请求,
472
456
  # 如果连续 5 次仍然失败,LAMDA 会放弃尝试并继续启动。
473
457
  #
474
458
  # 当然,可以自定义重试次数但是注意,如果服务器持续无响应,LAMDA 也将永远卡在这里
475
459
  # 什么时候需要设置重试次数:刚开机时设备可能并没有网络连接,如果你要在这时启动 LAMDA 你可以增大该值
476
- sh arm64-v8a/bin/launch.sh --properties.remote=http://example.com/config/properties.local --properties.tries=30
460
+ sh /data/server/bin/launch.sh --properties.remote=http://example.com/config/properties.local --properties.tries=30
477
461
  # 重试机制的每轮等待秒数n会随着重试次数的增加而增加。所以请谨慎设置该值。
478
462
  #
479
463
  # 如果你需要 LAMDA 监听到特定端口而不是 65000
480
464
  # 如果修改,请确保所有内网设备均以相同端口启动
481
465
  # 否则设备发现等功能无法正常工作
482
- sh arm64-v8a/bin/launch.sh --port=8123
466
+ sh /data/server/bin/launch.sh --port=8123
483
467
  # 请不要绑定 1024 以下的端口
484
468
  ```
485
469
 
@@ -505,7 +489,7 @@ LAMDA 对于自身数据的规划非常规范,绝对不会在你的系统中
505
489
 
506
490
  ```bash
507
491
  # 删除 LAMDA 相关目录
508
- rm -rf /data/local/tmp/arm64-v8a /data/usr
492
+ rm -rf /data/server /data/usr
509
493
  # 重启设备
510
494
  reboot
511
495
  ```
@@ -520,7 +504,7 @@ reboot
520
504
 
521
505
  远程桌面功能仅为 Chrome 95+ 设计,不支持多人访问,不保证兼容所有浏览器,如遇功能不正常请使用 Chrome。
522
506
 
523
- 在浏览器中打开链接 `http://192.168.0.2:65000` 可进入 web 远程桌面,你可以在此操作设备以及通过该界面的root模拟终端执行命令。如果启动服务端时指定了PEM证书 `--certificate`,远程桌面将需要你输入密码才能继续访问,你可以在PEM证书最后一行找到这个32位的固定密码。
507
+ 在浏览器中打开链接 `http://192.168.0.2:65000` 可进入 web 远程桌面,你可以在此操作设备以及通过该界面的root模拟终端执行命令。如果启动服务端时指定了PEM证书 `--certificate`,远程桌面将需要你输入密码才能继续访问,并且你需要将 `http://` 改为 `https://` 使用 HTTPS 的方式访问,你可以使用文本编辑器在PEM证书第一行找到这个固定密码。
524
508
 
525
509
  你也可以自定义远程桌面的 视频帧率(fps)、分辨率缩放比例(res)以及图像质量(quality)。同时,支持 H.264 软编码(部分情况下使用流量更少更流畅,仅支持最新版 Chrome 浏览器)。你可以通过远程桌面右上角的小齿轮进行调整,但是请注意,调整以上参数并不一定会产生正向效果,请依据事实调整。
526
510
 
@@ -529,7 +513,7 @@ reboot
529
513
 
530
514
  ## 文件上传
531
515
 
532
- 你可以在此页面直接**拖动文件或目录到右侧终端**上来上传文件/文件夹到设备,支持同时拖动多个文件或文件夹,单个文件最大不得超过 256MB,最多只支持同时上传 2k 个文件,上传的任何文件权限均为 644,文件将始终上传到 `/data/local/tmp` 目录下。
516
+ 你可以在此页面直接**拖动文件或目录到右侧终端**上来上传文件/文件夹到设备,支持同时拖动多个文件或文件夹,单个文件最大不得超过 256MB,最多只支持同时上传 2k 个文件,上传的任何文件权限均为 644,文件将始终上传到 `/data/usr/uploads` 目录下。
533
517
 
534
518
  ## 文件下载
535
519
 
@@ -543,6 +527,7 @@ LAMDA 的 tunnel2 功能,支持你将运行 LAMDA 的设备作为 http 网络
543
527
  ```bash
544
528
  # 默认代理无需任何认证,但是当你使用了 --certificate 启动时
545
529
  # 那么登录用户名为: lamda,密码与远程桌面登录令牌 (token) 相同
530
+ # 建议使用自定义配置 tunnel2.password 自行设置密码
546
531
  curl -x http://192.168.0.2:65000 https://httpbin.org/ip
547
532
  ```
548
533
 
@@ -809,10 +794,10 @@ device = d.frida
809
794
  device.enumerate_processes()
810
795
  ```
811
796
 
812
- 等效于
797
+ 等效原生代码
813
798
 
814
799
  ```python
815
- # 只是示例,请尽量使用上述方法连接
800
+ # 仅做示例,为了通用性,请务必使用上述方法
816
801
  manager = frida.get_device_manager()
817
802
  device = manager.add_remote_device("192.168.0.2:65000")
818
803
  device.enumerate_processes()
@@ -825,33 +810,10 @@ device.enumerate_processes()
825
810
  ```bash
826
811
  frida -H 192.168.0.2:65000 -f com.android.settings
827
812
  # 如果你在服务端启动时指定了 certificate 选项,请注意也需要在此加入 --certificate 参数例如
828
- frida -H 192.168.0.2:65000 -f com.android.settings --certificate /path/to/lamda.pem
813
+ # 且需要提供 token,这个 token 可以在 lamda.pem 的最后一行找到
814
+ frida -H 192.168.0.2:65000 -f com.android.settings --certificate /path/to/lamda.pem --token f141bce852f70730506f995991450adb
829
815
  ```
830
816
 
831
- 对于 objection 以及 r0capture 等,这些第三方工具可能并不会完全遵循原生 frida 工具的命令行用法,如果你需要使用这些第三方工具,需要确保 LAMDA 启动时**没有使用** `--certificate` 参数(加密传输),因为这些工具可能并没有可以传递PEM证书的参数。
832
-
833
- ```bash
834
- # objection 示例连接方法 (-N -h 192.168.0.2 -p 65000)
835
- objection -N -h 192.168.0.2 -p 65000 -g com.android.settings explore
836
- ```
837
-
838
- ```bash
839
- # r0capture 示例连接方法 (-H 192.168.0.2:65000)
840
- python3 r0capture.py -H 192.168.0.2:65000 -f com.some.package
841
- ```
842
-
843
- ```bash
844
- # jnitrace 示例连接方法 (-R 192.168.0.2:65000)
845
- jnitrace -R 192.168.0.2:65000 -l libc.so com.some.package
846
- ```
847
-
848
- ```bash
849
- # frida-dexdump 示例连接方法 (-H 192.168.0.2:65000)
850
- frida-dexdump -H 192.168.0.2:65000 -p PID
851
- ```
852
-
853
- 其他未提及的第三方工具请自行查看其使用方法。
854
-
855
817
  ## 使用 FRIDA 暴露 Java 接口
856
818
 
857
819
  这个功能类似于 [virjar/sekiro](https://github.com/virjar/sekiro),关于它的用途请参考 virjar 大佬的
@@ -859,7 +821,7 @@ frida-dexdump -H 192.168.0.2:65000 -p PID
859
821
 
860
822
  > 请转到 tools 目录查看使用方法。
861
823
 
862
- 此功能需要你能熟练编写 frida 脚本。示例中使用的脚本请参照 test-fridarpc.js 文件,特别注意: frida 脚本中 rpc.exports 定义的函数参数以及返回值只能为 int/float/string/list/jsdict 或者任意 js 中**可以被 JSON序列化**的值。假设设备IP为 192.168.0.2。
824
+ 此功能需要你能熟练编写 frida 脚本。示例中使用的脚本请参照 test-fridarpc.js 文件,特别注意: frida 脚本中 rpc.exports 定义的函数参数以及返回值只能为 int/float/string/list/map 或者任意 js 中**可以被 JSON序列化**的值。假设设备IP为 192.168.0.2。
863
825
 
864
826
  > 执行以下命令注入 RPC 到 com.android.settings(注意查看是否有报错),下面的相关文件在 tools 目录
865
827
 
@@ -867,7 +829,7 @@ frida-dexdump -H 192.168.0.2:65000 -p PID
867
829
  python3 fridarpc.py -f test-fridarpc.js -a com.android.settings -d 192.168.0.2
868
830
  ```
869
831
 
870
- 现在已经将接口暴露出来了,只需要请求 `http://192.168.0.2:65000/fridarpc/myRpcName/getMyString?args=["A","B"]` 即可得到脚本内方法的返回结果,链接也可以用浏览器打开,接口同时支持 POST 以及 GET,参数列表也可以同时使用多个参数,空列表代表无参数,注意这里的 args 参数序列化后的字符串最长**不能超过** `32KB`。
832
+ 现在已经将接口暴露出来了,只需要请求 `http://192.168.0.2:65000/fridarpc/myRpcName/getMyString?args=["A","B"]` 即可得到脚本内方法的返回结果,链接也可以用浏览器打开,接口同时支持 POST 以及 GET,参数列表也可以同时使用多个参数,空列表代表无参数,注意这里的 args 参数序列化后的字符串最长**不能超过** `32KB`(在使用了 --certificate 的情况下,链接需要改为 https 方式)。
871
833
 
872
834
  链接中的两个字符串参数 "A", "B" 即为注入的脚本中的方法 `getMyString(paramA, paramB)` 的位置参数。
873
835
 
@@ -920,6 +882,7 @@ print (res.status_code, res.json()["result"])
920
882
  > 首先在你的公网服务器上执行以下命令启动 frps(注意你可能还需要配置防火墙)
921
883
 
922
884
  ```bash
885
+ # frps 版本需要 > v0.45.0
923
886
  frps --token lamda --bind_addr 0.0.0.0 --bind_port 6009 --proxy_bind_addr 127.0.0.1 --allow_ports 10000-15000
924
887
  ```
925
888
 
@@ -961,6 +924,80 @@ d = Device("127.0.0.1", port=12345)
961
924
  改为 `--proxy_bind_addr 0.0.0.0`,这将导致 12345 端口直接绑定到公网。如果你未使用PEM证书启动 lamda,任何人都将可以访问,这是**非常非常危险**的。
962
925
  其次需要注意,web 远程桌面的流量始终都是 http 的,如果有人在你和服务器通信之间进行中间人,你的登录凭证可能会被窃取。当然,如果此期间不用 web 桌面将不存在这个问题。
963
926
 
927
+ ## 读写内置键值存储器
928
+
929
+ > Storage 是 LAMDA 内置的键值存储,它具有持久性,即使 LAMDA 重启,你依然可以在下次 LAMDA 启动时读取这些变量。
930
+ > 该 Storage 让你可以在设备中持久化存储信息以供不同的 client API 进程读取。
931
+
932
+ Storage 的总容量为 128MB,请勿用来存储大量数据。
933
+
934
+ ```python
935
+ # 获取一个 Storage 对象
936
+ storage = d.stub("Storage")
937
+
938
+ # 清空 Storage 中的所有信息(包括容器)
939
+ storage.clear()
940
+
941
+ # 清除名为 container_name 的容器中存储的所有键值
942
+ storage.remove("container_name")
943
+
944
+ # 获取一个键值容器对象
945
+ container = storage.use("container_name")
946
+
947
+ # 存储在 Storage 中的 key_name 和 container_name 是安全的
948
+ # 无法通过任何方式读取原始字符串,你必须完整知道容器名称以及 key 才能从容器中读取数据
949
+ # 如果还需要安全的存储值,比如,当该设备会被其他人使用,但是你不想存储的配置被其他人读取,
950
+ # 你可以像以下示例,提供加解密方法,这样即使 LAMDA 被非法访问
951
+ # 非预期的访问者也无法解密容器中存储的任何明文信息
952
+ from lamda.client import FernetCryptor
953
+ # 获取键值容器对象,对该容器的读写均通过 FernetCryptor 加解密
954
+ container = storage.use("container_name", cryptor=FernetCryptor, key="this_is_password")
955
+
956
+ # 当然,你也可以自己编写加解密流程
957
+ from lamda.client import BaseCryptor
958
+ class MyCryptor(BaseCryptor):
959
+ def __init__(self, cryptor_arg=0):
960
+ # 这里写入你的加解密初始化过程
961
+ def encrypt(self, data):
962
+ # 这里写入你的加密过程
963
+ return data
964
+ def decrypt(self, data):
965
+ # 这里写入你的解密过程
966
+ return data
967
+ # 获取键值容器对象,对该容器的读写均通过 MyCryptor 加解密
968
+ container = storage.use("container_name", cryptor=MyCryptor, cryptor_arg=999)
969
+
970
+
971
+ # 获取 key_name 的值(如果不存在,则返回 None)
972
+ container.get("key_name")
973
+
974
+ # 获取 key_name 的生存时间(-2 为该键不存在,-1 为无限生存时间)
975
+ # 其他正整数则为该 key 的剩余存活秒数
976
+ container.ttl("key_name")
977
+
978
+ # 设置 key_name 的值为 "value",并且 10 秒后自动删除
979
+ container.setex("key_name", "value", 10)
980
+
981
+ # 设置 key_name 的生存时间为 60 秒
982
+ # 60 秒后,该键值将自动被删除
983
+ container.expire("key_name", 60)
984
+
985
+ # 仅当 key_name 不存在时设置该键值
986
+ container.setnx("key_name", "value")
987
+
988
+ # 设置 key_name 的值为 "value"
989
+ # 其中,值支持任何 msgpack 可序列化的变量
990
+ container.set("key_name", [1, 2, 3])
991
+ container.set("key_name", {"john": "due"})
992
+ container.set("key_name", b"value")
993
+ container.set("key_name", "value")
994
+
995
+ # 检查 key_name 是否存在于容器中
996
+ container.exists("key_name")
997
+ # 删除 key_name
998
+ container.delete("key_name")
999
+ ```
1000
+
964
1001
 
965
1002
  ## 读写系统属性
966
1003
 
@@ -1146,24 +1183,24 @@ fd = open("写入到的本地文件", "wb")
1146
1183
  d.download_fd("/verity_key", fd)
1147
1184
 
1148
1185
  # 上传文件到设备
1149
- d.upload_file("本地文件路径.txt", "/data/local/tmp/上传到设备上的文件.txt")
1186
+ d.upload_file("本地文件路径.txt", "/data/usr/上传到设备上的文件.txt")
1150
1187
 
1151
1188
  # 从 内存/已打开的文件 上传文件
1152
1189
  from io import BytesIO
1153
- d.upload_fd(BytesIO(b"fileContent"), "/data/local/tmp/上传到设备上的文件.txt")
1190
+ d.upload_fd(BytesIO(b"fileContent"), "/data/usr/上传到设备上的文件.txt")
1154
1191
 
1155
1192
  # 注意必须使用 rb 模式打开文件
1156
1193
  fd = open("myfile.txt", "rb")
1157
- d.upload_fd(fd, "/data/local/tmp/上传到设备上的文件.txt")
1194
+ d.upload_fd(fd, "/data/usr/上传到设备上的文件.txt")
1158
1195
 
1159
1196
  # 删除设备上的文件
1160
- d.delete_file("/data/local/tmp/文件.txt")
1197
+ d.delete_file("/data/usr/文件.txt")
1161
1198
 
1162
1199
  # 修改设备上的文件权限
1163
- d.file_chmod("/data/local/tmp/文件.txt", mode=0o777)
1200
+ d.file_chmod("/data/usr/文件.txt", mode=0o777)
1164
1201
 
1165
1202
  # 获取设备上文件的信息
1166
- d.file_stat("/data/local/tmp/文件.txt")
1203
+ d.file_stat("/data/usr/文件.txt")
1167
1204
  ```
1168
1205
 
1169
1206
  ## 关机重启
@@ -1856,6 +1893,71 @@ t = 切换到 /data/local/tmp
1856
1893
  这里不会介绍如何使用这些命令或库。
1857
1894
 
1858
1895
 
1896
+ ## 使用 Debian 环境扩展模块
1897
+
1898
+ LAMDA 可以通过一个模块创建可在安卓内使用的完整 Debian 环境,你可以使用 apt 安装软件以及进行代码编译,同样,你可以在此环境中自行编译及使用 bpf 相关程序。你可以在 release 页面中找到 `lamda-mod-debian-arm64-v8a.tar.gz`(请根据你的机器架构下载对应的安装包)。
1899
+ 然后通过远程桌面或者 内置 adb 等方式,将 lamda-mod-debian-arm64-v8a.tar.gz 上传到设备,随后进行如下安装操作。
1900
+
1901
+ > 注:该 debian 环境只包含基础的软件包,你需要使用 apt 自行安装 git、python3 等常用命令。
1902
+
1903
+ ```bash
1904
+ # 切换到用户模块目录
1905
+ cd /data/usr/modules
1906
+ # 假设,该文件被你上传到了 /data/local/tmp
1907
+ tar -xzf /data/local/tmp/lamda-mod-debian-arm64-v8a.tar.gz
1908
+ # 解包完成
1909
+ ```
1910
+
1911
+ 解包完成后,当前目录下将会存在一个 `debian` 目录,这时,你已经完成了基本的安装,下面介绍如何进入系统。
1912
+
1913
+ ```bash
1914
+ # 首先我们需要获知绝对目录,在以上情况下,绝对目录为 /data/usr/modules/debian
1915
+ # 注意:每个根(debian 根系统)同时只支持一个终端实例使用
1916
+ # 执行以下命令进入 debian interactive shell
1917
+ debian /bin/bash
1918
+ # 执行一次 id 命令
1919
+ debian /bin/bash -c id
1920
+ #
1921
+ # 如果你并没有将模块安装于 /data/usr/modules,则需要指定模块位置
1922
+ debian --root /path/to/debian /bin/bash
1923
+ ```
1924
+
1925
+ 下面介绍进阶使用方法,你可以使用此 debian 环境自行建立一个 ssh 服务器,或者在此 debian 环境中运行 Python 脚本。
1926
+ 由于每个独立的 debian 环境只支持一个终端实例使用,我们建议用 ssh 的方式,这样,你可以接入多个 shell 到此 debian 环境。
1927
+ 什么是 `只支持一个终端实例使用`?就是当你执行 `debian /bin/bash` 后并保持使用状态,如果你在其他地方继续执行此命令,
1928
+ 该命令将会返回错误,使你无法再次进入此根系统,除非你将之前的 `debian /bin/bash` 退出。
1929
+
1930
+ 现在,我们介绍如何在此 debian 环境中运行一个 ssh 服务以及安装基础的 Python 环境。
1931
+
1932
+ ```bash
1933
+ # 执行命令进入 debian shell
1934
+ debian /bin/bash
1935
+ # 现在,你应该在 debian shell 中,执行以下命令
1936
+ root@localhost: apt update
1937
+ root@localhost: apt install -y openssh-server procps python3 python3-pip python3-dev
1938
+ root@localhost: echo 'PermitRootLogin yes' >>/etc/ssh/sshd_config
1939
+ root@localhost: echo 'StrictModes no' >>/etc/ssh/sshd_config
1940
+ root@localhost: mkdir -p /run/sshd
1941
+ root@localhost: # 修改 root 密码
1942
+ root@localhost: echo root:lamda|chpasswd
1943
+ root@localhost: # 退出 debian 环境
1944
+ root@localhost: exit
1945
+ # 现在,你已经进入了 lamda 的 shell 环境,执行以下命令来启动 debian 环境中的 ssh 服务
1946
+ debian /usr/sbin/sshd -D -e
1947
+ ```
1948
+
1949
+ 如果你想此 debian ssh 环境随 lamda 一同启动,请执行 `crontab -e`,并在其中写下如下规则并重启即可。
1950
+
1951
+ ```bash
1952
+ @reboot debian /usr/sbin/sshd -D -e >/data/usr/sshd.log 2>&1
1953
+ ```
1954
+
1955
+ 现在,获取该设备的 IP 地址,随后在你的电脑上执行如下命令并输入密码 lamda 即可登录该 debian shell
1956
+
1957
+ ```bash
1958
+ ssh root@192.168.x.x (手机IP)
1959
+ ```
1960
+
1859
1961
  # 工具及教程
1860
1962
 
1861
1963
  其中的每个文件夹下都有一份使用说明。
@@ -1897,4 +1999,4 @@ t = 切换到 /data/local/tmp
1897
1999
  打开 [tools/README.md](tools/README.md) 查看。
1898
2000
 
1899
2001
 
1900
- 如果仍有疑问,请加入社区讨论:[电报 t.me/lamda_dev](https://t.me/lamda_dev) | [gitter.im/lamda-dev](https://gitter.im/lamda-dev/community)
2002
+ 如果仍有疑问,请加入社区讨论:[电报 t.me/lamda_dev](https://t.me/lamda_dev)
@@ -2,4 +2,4 @@
2
2
  #
3
3
  # Distributed under MIT license.
4
4
  # See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
5
- __version__ = "3.155"
5
+ __version__ = "5.0"
@@ -10,18 +10,26 @@ import copy
10
10
  import time
11
11
  import uuid
12
12
  import json
13
+ import base64
14
+ import hashlib
13
15
  import platform
14
16
  import warnings
15
17
  import builtins
16
18
  import logging
19
+ import msgpack
20
+ # fix protobuf>=4.0/win32, #10158
21
+ if sys.platform == "win32":
22
+ os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
17
23
  import grpc
18
24
 
25
+ import pem as Pem
19
26
  import collections.abc
20
- # fix pyreadline on windows py310
27
+ # fix pyreadline, py310, Windows
21
28
  collections.Callable = collections.abc.Callable
22
29
 
23
30
  from urllib.parse import quote
24
31
  from collections import defaultdict
32
+ from cryptography.fernet import Fernet
25
33
  from os.path import basename, dirname, expanduser, join as joinpath
26
34
  from google.protobuf.json_format import MessageToDict, MessageToJson
27
35
  from grpc_interceptor import ClientInterceptor
@@ -56,6 +64,8 @@ __all__ = [
56
64
  "Keys",
57
65
  "KeyCode",
58
66
  "KeyCodes",
67
+ "BaseCryptor",
68
+ "FernetCryptor",
59
69
  "OpenVPNAuth",
60
70
  "OpenVPNEncryption",
61
71
  "OpenVPNKeyDirection",
@@ -229,11 +239,39 @@ def to_dict(prot):
229
239
  return json.loads(r)
230
240
 
231
241
 
242
+ class BaseCryptor(object):
243
+ def __str__(self):
244
+ return "{}".format(self.__class__.__name__)
245
+ __repr__ = __str__
246
+ def encrypt(self, data):
247
+ return data
248
+ def decrypt(self, data):
249
+ return data
250
+
251
+
232
252
  class BaseServiceStub(object):
253
+ def __str__(self):
254
+ return "{}".format(self.__class__.__name__)
255
+ __repr__ = __str__
233
256
  def __init__(self, stub):
234
257
  self.stub = stub
235
258
 
236
259
 
260
+ class FernetCryptor(BaseCryptor):
261
+ def __init__(self, key=None):
262
+ key = self._get_key(key)
263
+ self.encoder = Fernet(key)
264
+ def encrypt(self, data):
265
+ return self.encoder.encrypt(data)
266
+ def decrypt(self, data):
267
+ return self.encoder.decrypt(data)
268
+ def _get_key(self, key):
269
+ key = (key or "").encode()
270
+ key = hashlib.sha256(key).digest()
271
+ key = base64.b64encode(key)
272
+ return key
273
+
274
+
237
275
  class ClientLoggingInterceptor(ClientInterceptor):
238
276
  def truncate_string(self, s):
239
277
  return "{:.1024}...".format(s) if len(s) > 1024 else s
@@ -251,18 +289,14 @@ class ClientLoggingInterceptor(ClientInterceptor):
251
289
 
252
290
 
253
291
  class ClientSessionMetadataInterceptor(ClientInterceptor):
254
- def get_instance_ID(self):
255
- return "{:06d}{:010d}".format(os.getpid(), id(self))
256
-
292
+ def __init__(self, session):
293
+ super(ClientSessionMetadataInterceptor, self).__init__()
294
+ self.session = session
257
295
  def intercept(self, function, request, details):
258
- """
259
- 在每次远程调用加上本实例的ID用于实现锁功能
260
- """
261
296
  metadata = {}
262
297
  metadata["version"] = __version__
263
- metadata["instance"] = self.get_instance_ID()
298
+ metadata["instance"] = self.session
264
299
  metadata["hostname"] = quote(platform.node())
265
- metadata["python_branch"] = platform.python_branch()
266
300
  details = details._replace(metadata=metadata.items())
267
301
  res = function(request, details)
268
302
  return res
@@ -286,7 +320,7 @@ class GrpcRemoteExceptionInterceptor(ClientInterceptor):
286
320
  return clazz(*args)
287
321
 
288
322
  def raise_remote_exception(self, res):
289
- metadata = dict(res.initial_metadata())
323
+ metadata = dict(res.initial_metadata() or [])
290
324
  exception = metadata.get("exception", None)
291
325
  if exception != None:
292
326
  raise self.remote_exception(exception)
@@ -371,8 +405,6 @@ class ObjectUiAutomatorOpStub:
371
405
  r = self.stub.selectorClickExists(req)
372
406
  return r.value
373
407
  def click_exist(self, *args, **kwargs):
374
- # deprecated
375
- warnings.warn("use d(..).click_exists() instead", DeprecationWarning)
376
408
  return self.click_exists(*args, **kwargs)
377
409
  def long_click(self, corner=Corner.COR_CENTER):
378
410
  """
@@ -390,8 +422,6 @@ class ObjectUiAutomatorOpStub:
390
422
  r = self.stub.selectorExists(req)
391
423
  return r.value
392
424
  def exist(self, *args, **kwargs):
393
- # deprecated
394
- warnings.warn("use d(..).exists() instead", DeprecationWarning)
395
425
  return self.exists(*args, **kwargs)
396
426
  def info(self):
397
427
  """
@@ -837,7 +867,7 @@ class UiAutomatorStub(BaseServiceStub):
837
867
  return ObjectUiAutomatorOpStub(self.stub, kwargs)
838
868
 
839
869
 
840
- class ObjectApplicationOpStub:
870
+ class ApplicationOpStub:
841
871
  def __init__(self, stub, applicationId):
842
872
  """
843
873
  Application 子接口,用来模拟出实例的意味
@@ -845,7 +875,8 @@ class ObjectApplicationOpStub:
845
875
  self.applicationId = applicationId
846
876
  self.stub = stub
847
877
  def __str__(self):
848
- return "Application: {}".format(self.applicationId)
878
+ return "{}:{}".format(self.stub.__class__.__name__,
879
+ self.applicationId)
849
880
  __repr__ = __str__
850
881
  def is_foreground(self):
851
882
  """
@@ -907,6 +938,8 @@ class ObjectApplicationOpStub:
907
938
  req = protos.ApplicationRequest(name=self.applicationId)
908
939
  r = self.stub.resetApplicationData(req)
909
940
  return r.value
941
+ def reset(self):
942
+ return self.reset_data()
910
943
  def start(self):
911
944
  """
912
945
  启动应用
@@ -1018,7 +1051,118 @@ class ApplicationStub(BaseServiceStub):
1018
1051
  r = self.stub.startActivity(req)
1019
1052
  return r.value
1020
1053
  def __call__(self, applicationId):
1021
- return ObjectApplicationOpStub(self.stub, applicationId)
1054
+ return ApplicationOpStub(self.stub, applicationId)
1055
+
1056
+
1057
+ class StorageOpStub:
1058
+ def __str__(self):
1059
+ return "{}:{}".format(self.stub.__class__.__name__,
1060
+ self.name)
1061
+ __repr__ = __str__
1062
+ # 用于容器值序列化的方法
1063
+ def _decrypt(self, data):
1064
+ return self.cryptor.decrypt(data)
1065
+ def _encrypt(self, data):
1066
+ return self.cryptor.encrypt(data)
1067
+ def _unpack(self, value):
1068
+ return msgpack.loads(self._decrypt(value))
1069
+ def _pack(self, value):
1070
+ return self._encrypt(msgpack.dumps(value))
1071
+ # 注意:此接口可能并不是跨语言通用
1072
+ def __init__(self, stub, name, cryptor=None):
1073
+ self.cryptor = cryptor
1074
+ self.name = name
1075
+ self.stub = stub
1076
+ def delete(self, key):
1077
+ """
1078
+ 删除一个 KEY
1079
+ """
1080
+ req = protos.StorageRequest(key=key)
1081
+ req.container = self.name
1082
+ res = self.stub.delete(req)
1083
+ return res.value
1084
+ def exists(self, key):
1085
+ """
1086
+ 检查一个 KEY 是否存在
1087
+ """
1088
+ req = protos.StorageRequest(key=key)
1089
+ req.container = self.name
1090
+ res = self.stub.exists(req)
1091
+ return res.value
1092
+ def get(self, key, default=None):
1093
+ """
1094
+ 获取 KEY 对应的键值
1095
+ """
1096
+ req = protos.StorageRequest(key=key)
1097
+ req.container = self.name
1098
+ val = self.stub.get(req).value
1099
+ res = self._unpack(val) if val else default
1100
+ return res
1101
+ def set(self, key, value):
1102
+ """
1103
+ 设置 KEY 对应的键值
1104
+ """
1105
+ value = self._pack(value)
1106
+ req = protos.StorageRequest(key=key, value=value)
1107
+ req.container = self.name
1108
+ res = self.stub.set(req)
1109
+ return res.value
1110
+ def setex(self, key, value, ttl):
1111
+ """
1112
+ 设置 KEY 对应的键值,该 KEY 在 TTL 秒后自动删除
1113
+ """
1114
+ value = self._pack(value)
1115
+ req = protos.StorageRequest(key=key, value=value)
1116
+ req.container = self.name
1117
+ req.ttl = ttl
1118
+ res = self.stub.setex(req)
1119
+ return res.value
1120
+ def setnx(self, key, value):
1121
+ """
1122
+ 设置 KEY 对应的键值 (仅当该键不存在时)
1123
+ """
1124
+ value = self._pack(value)
1125
+ req = protos.StorageRequest(key=key, value=value)
1126
+ req.container = self.name
1127
+ res = self.stub.setnx(req)
1128
+ return res.value
1129
+ def expire(self, key, ttl):
1130
+ """
1131
+ 设置 KEY 在 TTL 秒后过期
1132
+ """
1133
+ req = protos.StorageRequest(key=key, ttl=ttl)
1134
+ req.container = self.name
1135
+ res = self.stub.expire(req)
1136
+ return res.value
1137
+ def ttl(self, key):
1138
+ """
1139
+ 获取 KEY 的 TTL (过期时间)
1140
+ """
1141
+ req = protos.StorageRequest(key=key)
1142
+ req.container = self.name
1143
+ res = self.stub.ttl(req)
1144
+ return res.value
1145
+
1146
+
1147
+ class StorageStub(BaseServiceStub):
1148
+ def clear(self):
1149
+ """
1150
+ 删除所有 Storage 容器
1151
+ """
1152
+ r = self.stub.clearAll(protos.Empty())
1153
+ return r.value
1154
+ def use(self, name, cryptor=BaseCryptor, **kwargs):
1155
+ """
1156
+ 使用一个 Storage 容器
1157
+ """
1158
+ return StorageOpStub(self.stub, name, cryptor(**kwargs))
1159
+ def remove(self, name):
1160
+ """
1161
+ 删除一个 Storage 容器
1162
+ """
1163
+ req = protos.String(value=name)
1164
+ r = self.stub.clearContainer(req)
1165
+ return r.value
1022
1166
 
1023
1167
 
1024
1168
  class UtilStub(BaseServiceStub):
@@ -1075,11 +1219,12 @@ class UtilStub(BaseServiceStub):
1075
1219
  """
1076
1220
  r = self.stub.shutdown(protos.Empty())
1077
1221
  return r.value
1078
- def reload(self):
1222
+ def reload(self, clean=False):
1079
1223
  """
1080
1224
  重载设备上运行的服务端
1081
1225
  """
1082
- r = self.stub.reload(protos.Empty())
1226
+ req = protos.Boolean(value=clean)
1227
+ r = self.stub.reload(req)
1083
1228
  return r.value
1084
1229
  def exit(self):
1085
1230
  """
@@ -1500,6 +1645,18 @@ class LockStub(BaseServiceStub):
1500
1645
  req = protos.Integer(value=leaseTime)
1501
1646
  r = self.stub.acquireLock(req)
1502
1647
  return r.value
1648
+ def get_locking_session(self):
1649
+ """
1650
+ 获取当前占有设备锁的会话ID
1651
+ """
1652
+ r = self.stub.getLockingSession(protos.Empty())
1653
+ return r.value
1654
+ def get_session_token(self):
1655
+ """
1656
+ 获取当前占有设备锁的会话的令牌
1657
+ """
1658
+ r = self.stub.getSessionToken(protos.Empty())
1659
+ return r.value
1503
1660
  def refresh_lock(self, leaseTime=60):
1504
1661
  """
1505
1662
  刷新用于控制设备的锁,应该在定时任务每60s内调用以保持会话
@@ -1606,50 +1763,65 @@ class WifiStub(BaseServiceStub):
1606
1763
 
1607
1764
  class Device(object):
1608
1765
  def __init__(self, host, port=65000,
1609
- certificate=None):
1766
+ certificate=None,
1767
+ session=None):
1610
1768
  self.certificate = certificate
1611
1769
  self.server = "{0}:{1}".format(host, port)
1612
1770
  if certificate is not None:
1613
1771
  with open(certificate, "rb") as fd:
1614
- cer = fd.read()
1615
- creds = grpc.ssl_channel_credentials(cer)
1616
- chann = grpc.secure_channel(self.server, creds,
1772
+ key, crt, ca = self._parse_certdata(fd.read())
1773
+ creds = grpc.ssl_channel_credentials(root_certificates=ca,
1774
+ certificate_chain=crt,
1775
+ private_key=key)
1776
+ self._chan = grpc.secure_channel(self.server, creds,
1617
1777
  options=(("grpc.ssl_target_name_override",
1618
- self._ssl_common_name(cer)),
1778
+ self._parse_cname(crt)),
1619
1779
  ("grpc.enable_http_proxy",
1620
- False)))
1780
+ 0)))
1621
1781
  else:
1622
- chann = grpc.insecure_channel(self.server)
1623
- interceptors = [ClientSessionMetadataInterceptor(),
1782
+ self._chan = grpc.insecure_channel(self.server)
1783
+ session = session or uuid.uuid4().hex
1784
+ interceptors = [ClientSessionMetadataInterceptor(session),
1624
1785
  GrpcRemoteExceptionInterceptor(),
1625
1786
  ClientLoggingInterceptor()]
1626
- self.chann = grpc.intercept_channel(chann,
1787
+ self.channel = grpc.intercept_channel(self._chan,
1627
1788
  *interceptors)
1789
+ self.session = session
1628
1790
  @property
1629
1791
  def frida(self):
1630
1792
  if _frida_dma is None:
1631
1793
  raise ModuleNotFoundError("frida")
1794
+ kwargs = {}
1632
1795
  if self.certificate is not None:
1633
- device = _frida_dma.add_remote_device(self.server,
1634
- certificate=self.certificate)
1635
- else:
1636
- device = _frida_dma.add_remote_device(self.server)
1796
+ kwargs["certificate"] = self.certificate
1797
+ if self._get_session_token():
1798
+ kwargs["token"] = self._get_session_token()
1799
+ device = _frida_dma.add_remote_device(self.server,
1800
+ **kwargs)
1637
1801
  return device
1638
1802
  def __str__(self):
1639
1803
  return "Device@{}".format(self.server)
1640
1804
  __repr__ = __str__
1641
- def _ssl_common_name(self, cer):
1642
- _, _, der = pem.unarmor(cer)
1805
+ def _parse_certdata(self, data):
1806
+ key, crt, ca = Pem.parse(data)
1807
+ ca = ca.as_bytes()
1808
+ crt = crt.as_bytes()
1809
+ key = key.as_bytes()
1810
+ return key, crt, ca
1811
+ def _parse_cname(self, crt):
1812
+ _, _, der = pem.unarmor(crt)
1643
1813
  subject = x509.Certificate.load(der).subject
1644
1814
  return subject.native["common_name"]
1645
1815
  def _get_service_stub(self, module):
1646
1816
  stub = getattr(services, "{0}Stub".format(module))
1647
- return stub(self.chann)
1817
+ return stub(self.channel)
1648
1818
  def stub(self, module):
1649
1819
  modu = sys.modules[__name__]
1650
1820
  stub = self._get_service_stub(module)
1651
1821
  wrap = getattr(modu, "{0}Stub".format(module))
1652
- return wrap(stub)
1822
+ inst = getattr(self, module, wrap(stub))
1823
+ self.__setattr__(module, inst)
1824
+ return inst
1653
1825
  # 快速调用: File
1654
1826
  def download_fd(self, fpath, fd):
1655
1827
  return self.stub("File").download_fd(fpath, fd)
@@ -1695,8 +1867,8 @@ class Device(object):
1695
1867
  return self.stub("Util").shutdown()
1696
1868
  def exit(self):
1697
1869
  return self.stub("Util").exit()
1698
- def reload(self):
1699
- return self.stub("Util").reload()
1870
+ def reload(self, clean=False):
1871
+ return self.stub("Util").reload(clean)
1700
1872
  def beep(self):
1701
1873
  return self.stub("Util").beep()
1702
1874
  def setprop(self, name, value):
@@ -1815,6 +1987,7 @@ class Device(object):
1815
1987
  return self.stub("UiAutomator").device_info()
1816
1988
  def __call__(self, **kwargs):
1817
1989
  return self.stub("UiAutomator")(**kwargs)
1990
+ # 日志打印
1818
1991
  def setup_log_format(self):
1819
1992
  logging.basicConfig(format=FORMAT)
1820
1993
  def set_debug_log_enabled(self, enable):
@@ -1822,6 +1995,10 @@ class Device(object):
1822
1995
  logger.setLevel(level)
1823
1996
  return enable
1824
1997
  # 接口锁定
1998
+ def _get_session_token(self):
1999
+ return self.stub("Lock").get_session_token()
2000
+ def _get_locking_session(self):
2001
+ return self.stub("Lock").get_locking_session()
1825
2002
  def _acquire_lock(self, leaseTime=60):
1826
2003
  return self.stub("Lock").acquire_lock(leaseTime)
1827
2004
  def _refresh_lock(self, leaseTime=60):
@@ -36,9 +36,11 @@ class StaleObjectException(Exception):
36
36
  """ Exception """
37
37
  class StartupActivityNotFound(Exception):
38
38
  """ Exception """
39
+ class StorageOutOfMemory(Exception):
40
+ """ Exception """
39
41
  class UiAutomatorException(Exception):
40
42
  """ Exception """
41
43
  class UiObjectNotFoundException(Exception):
42
44
  """ Exception """
43
45
  class UnHandledException(Exception):
44
- """ Exception """
46
+ """ Exception """
@@ -18,6 +18,7 @@ import public "settings.proto";
18
18
  import public "status.proto";
19
19
  import public "application.proto";
20
20
  import public "uiautomator.proto";
21
+ import public "storage.proto";
21
22
  import public "proxy.proto";
22
23
  import public "file.proto";
23
24
  import public "wifi.proto";
@@ -220,7 +221,7 @@ service Util {
220
221
  rpc installCACertificate(CertifiRequest) returns (Boolean) {}
221
222
  rpc uninstallCACertificate(CertifiRequest) returns (Boolean) {}
222
223
  rpc shutdown(google.protobuf.Empty) returns (Boolean) {}
223
- rpc reload(google.protobuf.Empty) returns (Boolean) {}
224
+ rpc reload(Boolean) returns (Boolean) {}
224
225
  rpc exit(google.protobuf.Empty) returns (Boolean) {}
225
226
  rpc setProp(SetPropRequest) returns (Boolean) {}
226
227
  rpc getProp(String) returns (String) {}
@@ -236,6 +237,21 @@ service File {
236
237
 
237
238
  service Lock {
238
239
  rpc releaseLock(google.protobuf.Empty) returns (Boolean) {}
240
+ rpc getLockingSession(google.protobuf.Empty) returns (String) {}
241
+ rpc getSessionToken(google.protobuf.Empty) returns (String) {}
239
242
  rpc acquireLock(Integer) returns (Boolean) {}
240
243
  rpc refreshLock(Integer) returns (Boolean) {}
244
+ }
245
+
246
+ service Storage {
247
+ rpc clearAll(StorageRequest) returns (Boolean) {}
248
+ rpc clearContainer(StorageRequest) returns (Boolean) {}
249
+ rpc exists(StorageRequest) returns (Boolean) {}
250
+ rpc delete(StorageRequest) returns (Boolean) {}
251
+ rpc expire(StorageRequest) returns (Boolean) {}
252
+ rpc ttl(StorageRequest) returns (Integer) {}
253
+ rpc set(StorageRequest) returns (Boolean) {}
254
+ rpc setnx(StorageRequest) returns (Boolean) {}
255
+ rpc setex(StorageRequest) returns (Boolean) {}
256
+ rpc get(StorageRequest) returns (Bytes) {}
241
257
  }
@@ -0,0 +1,13 @@
1
+ // Copyright 2022 rev1si0n (ihaven0emmail@gmail.com). All rights reserved.
2
+ //
3
+ // Distributed under MIT license.
4
+ // See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
5
+ syntax = "proto3";
6
+ package lamda.rpc;
7
+
8
+ message StorageRequest {
9
+ string container = 1;
10
+ string key = 2;
11
+ bytes value = 3;
12
+ uint32 ttl = 4;
13
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lamda
3
- Version: 3.155
3
+ Version: 5.0
4
4
  Summary: Android reverse engineering & automation framework
5
5
  Home-page: https://github.com/rev1si0n/lamda
6
6
  Author: rev1si0n
@@ -12,6 +12,6 @@ Classifier: Intended Audience :: Science/Research
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Operating System :: Android
14
14
  Classifier: Topic :: Security
15
- Requires-Python: >=3.6,<3.11
15
+ Requires-Python: >=3.6,<3.12
16
16
  Provides-Extra: next
17
17
  Provides-Extra: full
@@ -33,6 +33,7 @@ lamda/rpc/services.proto
33
33
  lamda/rpc/settings.proto
34
34
  lamda/rpc/shell.proto
35
35
  lamda/rpc/status.proto
36
+ lamda/rpc/storage.proto
36
37
  lamda/rpc/types.proto
37
38
  lamda/rpc/uiautomator.proto
38
39
  lamda/rpc/util.proto
@@ -1,7 +1,10 @@
1
- grpcio-tools<1.49.0,>=1.35.0
1
+ grpcio-tools<1.60.0,>=1.35.0
2
2
  grpc-interceptor<0.15.0,>=0.13.0
3
- grpcio<1.49.0,>=1.35.0
3
+ grpcio<1.60.0,>=1.35.0
4
+ cryptography>=35.0.0
5
+ msgpack>=1.0.0
4
6
  asn1crypto<2,>=1.0.0
7
+ pem==23.1.0
5
8
 
6
9
  [:sys_platform == "win32"]
7
10
  pyreadline==2.1
@@ -10,7 +10,7 @@ setuptools.setup(
10
10
  description = "Android reverse engineering & automation framework",
11
11
  url = "https://github.com/rev1si0n/lamda",
12
12
  author = "rev1si0n",
13
- python_requires = ">=3.6,<3.11",
13
+ python_requires = ">=3.6,<3.12",
14
14
  zip_safe = False,
15
15
  extras_require = {
16
16
  "next": ["frida>=16.0.0,<17.0.0"],
@@ -20,10 +20,13 @@ setuptools.setup(
20
20
  ],
21
21
  },
22
22
  install_requires= [
23
- "grpcio-tools>=1.35.0,<1.49.0",
23
+ "grpcio-tools>=1.35.0,<1.60.0",
24
24
  "grpc-interceptor>=0.13.0,<0.15.0",
25
- "grpcio>=1.35.0,<1.49.0",
25
+ "grpcio>=1.35.0,<1.60.0",
26
+ "cryptography>=35.0.0",
27
+ "msgpack>=1.0.0",
26
28
  "asn1crypto>=1.0.0,<2",
29
+ "pem==23.1.0",
27
30
  ],
28
31
  classifiers = [
29
32
  "Environment :: Console",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes