diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index bdf1758..f3c3726 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,39 @@
+7.90
+* 持久化脚本支持 spawn 模式
+* 支持持久化脚本输出日志
+* 修复 dump_window_hierarchy
+* 修复 frida 实例获取逻辑错误
+
+7.85
+* 支持 mDNS 广播服务
+* 支持枚举选择器选中的所有元素
+* 客户端添加自动重试机制
+* 修复 Bound 比较逻辑错误
+* 允许从远程加载证书
+
+7.80
+* 优化实时投屏流畅度
+* 新增持久化 Hook 脚本支持
+* 新增 Hook RPC 支持
+* 新增数据上报支持
+
+7.76
+* 修复工具版本依赖
+* 修复 Python 版本匹配问题
+* 更新部分子模块
+
+7.75
+* 新增 OCR识别接口
+* 新增 get_application_by_name
+* 更新部分子模块及依赖版本
+
+7.73
+* 修复部分应用白屏的问题
+
+7.72
+* 更新部分子模块
+* 已知问题修复
+
7.71
* 修复 Permission Loophole #95
* 修复 enumerate_all_pkg_names
diff --git a/DISCLAIMER.TXT b/DISCLAIMER.TXT
index 11b545c..0bbda71 100644
--- a/DISCLAIMER.TXT
+++ b/DISCLAIMER.TXT
@@ -1,34 +1,37 @@
-为了下载使用由 rev1si0n (账号 github.com/rev1si0n,邮箱 lamda.devel@gmail.com)(以下简称“本人”)
-个人开发的软件 LAMDA(以下简称“本服务”),您应当阅读并遵守本协议。请您务必审慎阅读、充分理解各条款内容,并选择接受或不接受。
-除非您已阅读并接受本协议所有条款,否则您将无权下载、安装或使用本软件及相关服务。
-您的下载、安装、使用等行为即视为您已阅读并同意受到本声明的约束。
+若您需要获得本服务,您(以下称"用户")应当同意本协议的全部条款并按照页面上的提示完成全部申请使用程序。您可以在源码或者发布程序中找到 DISCLAIMER.TXT,或者查看以下副本。
+
+为了下载和使用由 firerpa(地址:github.com/firerpa,邮箱:lamda.devel@gmail.com,以下简称“开发者”)开发的 LAMDA 软件(以下简称“本服务”),您必须仔细阅读并同意本协议中的所有条款。请确保您在下载、安装或使用本软件前,已充分理解并同意以下内容。
+在未完全阅读并接受本协议条款之前,您无权下载、安装或使用本软件及其相关服务。一旦您进行下载、安装或使用本软件的操作,即视为您已阅读并同意本协议的全部条款,并愿意遵守其约束。
风险告知:
本服务需要设备获取 root 权限才能运行,且默认的通讯协议及相关证书文件均为开放信息,这可能会增加您设备被入侵的风险。
-本服务由本人自行开发并使用,可能存在潜在的未知的风险如数据丢失、系统崩溃等,由用户自行决定是否下载、使用本服务。
+本服务可能存在未知的逻辑错误,可能会导致潜在的风险如数据丢失、系统崩溃等,由用户自行决定是否下载、使用本服务。
1、本服务设计目的为提高安全分析及测试人员工作效率,应用行为、应用合规分析等。提供的相关工具均为合法合规的APP测试分析、Mock 场景提供。
本服务本身不提供任何侵入、修改、抓取其他应用内存及网络数据的功能,整合了各大开源框架提供的服务供用户自行选择,方便安全分析人员使用,减少用户的重复性劳动以及管理成本。
本服务本身不存在盈利性,用户可根据自己需求自行通过下载获取使用,下载及使用过程中不会收取任何费用。
-2、本服务尊重并保护用户的个人隐私,不会窃取任何用户设备中的信息。本框架的启动及任何对设备数据读取、存储、传输等权利均在用户自己手中。
+2、本服务尊重并保护用户的个人隐私,不会窃取任何用户设备中的信息。本服务的启动及任何对设备数据读取、存储、传输等权利均在用户自己手中。
3、用户必须在无隐私数据的虚拟设备或者专用设备中使用本服务。在使用本服务时,必须遵守中华人民共和国或者用户所属国家或地区的法律法规,
不得为任何非法目的而使用本服务,不得利用本服务进行任何不利于他人的行为。
4、用户只可使用本服务进行正规的学习研究或是经过合法授权的应用分析、测试等行为,若用户在使用该软件服务的过程中违背以上原则对第三方造成损失,一切责任由该用户自行承担。
-5、任何单位或个人因下载使用本服务而产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),本人不承担任何法律责任。
+5、任何单位或个人因下载使用本服务而产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),开发者不承担任何法律责任。
-6、本服务仅供个人、教育、非商业性使用,您同意不会将本服务以及本服务提供的相关服务或接口用于任何商业目的,包括销售、盈利服务或产品开发。
+6、您可以将本服务用于商业用途,但仅限于通过本服务提供的功能、接口或相关服务进行衍生功能扩展或产品开发。您同意,不得将本服务及其相关服务或接口用于任何违反当地法律法规,或从事损害他人利益的行为。
-7、用户明确并同意本协议条款列举的全部内容,对使用本服务过程中可能存在的风险和后果由用户自行承担,本人不承担任何法律责任。
+7、用户明确并同意本协议条款列举的全部内容,对使用本服务过程中可能存在的风险和后果由用户自行承担,开发者不承担任何法律责任。
-8、本人有权随时对本声明条款及附件内容进行单方面的变更、中断或终止部分或全部本服务的权利。并以消息推送、网页公告等方式予以公布,
+8、开发者有权随时对本声明条款及附件内容进行单方面的变更、中断或终止部分或全部本服务的权利。并以消息推送、网页公告等方式予以公布,
公布后立即自动生效,无需另行单独通知;若您在本声明内容公告变更后继续使用的,表示您已充分阅读、理解并接受修改后的声明内容。
-9、如果本声明的任何部分被认为无效或不可执行,则该部分应以符合相关法律的方式予以修正,以尽可能地反映出本人的原始意图,
-其余部分仍具有完全效力。不可执行的部分声明,并不构成本人放弃执行该声明的权利。
+9、如果本声明的任何部分被认为无效或不可执行,则该部分应以符合相关法律的方式予以修正,以尽可能地反映出开发者的原始意图,
+其余部分仍具有完全效力。不可执行的部分声明,并不构成开发者放弃执行该声明的权利。
+
+10、保留权利:未明示授权的其他一切权利均由开发者所有。
+
-10、保留权利:未明示授权的其他一切权利均由本人所有。
\ No newline at end of file
+请确认您已阅读并接受本协议所有条款,否则您将无权下载、安装或使用本软件及相关服务。
\ No newline at end of file
diff --git a/README.md b/README.md
index 7e28661..9508b71 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Android reverse engineering & automation framework, the super power.
+安卓 RPA 机器人框架,下一代移动端数据自动化机器人
@@ -12,29 +12,17 @@
使用文档 | TELEGRAM | QQ 群组 | 更新历史
+使用文档 | TELEGRAM | QQ 群组 | 更新历史
-LAMDA 是一个**安卓领域的集大成者框架**,设计为减少**安全分析**及**应用测试**工作的时间和琐碎问题,以**编程化**的**接口**替代大量手动操作,**易部署**,没有那些复杂花哨不跨平台的安装流程,你所需要的能力他大概率能做到并且做的更好。他并不是一个单一功能的框架,他是集 Appium、uiautomator **自动化**的超集同时具备**逆向**领域如 **Hook** **抓包** **证书安装** **组网** **API跟踪** **手机自控** 等等各种能力的框架。为了让你大概了解它的用处:你是否会在手机上安装各类代理、插件或者点来点去的设置来完成你的工作?你是否要在异地操作远在千里之外的手机?你是否有编程控制手机的需求?是否还在某些云手机厂商那里购买昂贵的 **IP切换**、**远程ADB调试**、**RPA自动化**甚至连 **logcat 日志** 都要付费的服务?如果有,那么对了,只需一个 LAMDA 即可解决所有问题。并且,LAMDA 更注重**分布式**,事实上,你可以在一台公网服务器上管理散布在世界各地各种网络环境中的设备。当然,LAMDA 可以做到的远不止于此,你可以阅读 LAMDA 的使用文档尽情探索他的所有能力。 +智能机的崛起,传统网页端的普及度也开始显著减弱,数据与应用正加速向移动端转移。越来越多的人选择通过智能手机和平板等移动设备来获取信息和服务。随着移动设备的普及,用户享受到更便捷访问体验,传统的网页内容模式面临重新审视。与此同时,数据采集的技术也亟需适应这一趋势。过去,许多数据采集工具专注于网页内容,在移动端环境中,尤其是在移动端封闭的黑盒中,现今的常规采集技术也面临着新的挑战。LAMDA 的诞生,为这一切创造了可能。 -框架长期维护及更新,质量稳定,安全可靠,生产环境可用
现稳定应用于多个外部大型系统,包括自动化取证,云平台,信息采集,涉诈应用分析系统等
-
-
关注公众号查看视频教程以及更多使用方法
知识应该是共享的,我们不会要求你为相关知识付费
BILIBILI 同步发布
文字版文档请查看使用文档
-
丰富的设备编程接口
-LAMDA 提供多达 160 条编程 API 接口,让你可以对安卓设备进行无微不至的管理和操作,提供了包括命令执行,系统设置,系统状态,应用相关,自动化相关,代理以及文件等十几个大类的接口。同时提供了封装完整的 Python 库让你可以快速上手使用。
- -
-
-
提供多达 160 条编程 API 接口,让你可以对安卓设备进行无微不至的管理和操作,提供了包括命令执行,系统设置,系统状态,应用相关,自动化相关,代理以及文件等十几个大类的接口。同时提供了封装完整的 Python 库让你可以快速上手使用。
简洁易用的远程桌面
diff --git a/image/api.png b/image/api.png deleted file mode 100644 index dfc9e84..0000000 Binary files a/image/api.png and /dev/null differ diff --git a/image/overview.png b/image/overview.png deleted file mode 100644 index cfc4a82..0000000 Binary files a/image/overview.png and /dev/null differ diff --git a/image/totalview.png b/image/totalview.png deleted file mode 100644 index e3f663a..0000000 Binary files a/image/totalview.png and /dev/null differ diff --git a/lamda/__init__.py b/lamda/__init__.py index facbb11..cbf79fc 100644 --- a/lamda/__init__.py +++ b/lamda/__init__.py @@ -2,4 +2,4 @@ # # Distributed under MIT license. # See file LICENSE for detail or copy at https://opensource.org/licenses/MIT -__version__ = "7.71" +__version__ = "7.90" diff --git a/lamda/client.py b/lamda/client.py index 80d41f0..b646608 100644 --- a/lamda/client.py +++ b/lamda/client.py @@ -44,7 +44,9 @@ from . import __version__ from . types import AttributeDict, BytesIO -from . exceptions import UnHandledException, DuplicateEntryError, InvalidArgumentError +from . exceptions import (UnHandledException, DuplicateEntryError, + InvalidArgumentError, UiObjectNotFoundException, + IllegalStateException) from . import exceptions handler = logging.StreamHandler() @@ -55,6 +57,8 @@ sys.path.append(joinpath(dirname(__file__))) sys.path.append(joinpath(dirname(__file__), "rpc")) +# use native resolver to support mDNS +os.environ["GRPC_DNS_RESOLVER"] = "native" protos, services = grpc.protos_and_services("services.proto") __all__ = [ @@ -63,6 +67,8 @@ "GproxyType", "GrantType", "Group", + "CustomOcrBackend", + "OcrEngine", "Key", "Keys", "KeyCode", @@ -168,6 +174,20 @@ def center(b): y = int(b.top + (b.bottom - b.top)/2) return Point(x=x, y=y) +def contain(a, b): + return all([b.top >= a.top, + b.left >= a.left, + b.bottom <= a.bottom, + b.right <= a.right]) + +def equal(a, b): + if not isinstance(b, protos.Bound): + return False + return all([b.top == a.top, + b.left == a.left, + b.bottom == a.bottom, + b.right == a.right]) + def corner(b, position): ca, cb = position.split("-") return Point(x=getattr(b, cb), @@ -178,6 +198,8 @@ def corner(b, position): Direction = protos.Direction GproxyType = protos.GproxyType GrantType = protos.GrantType +ScriptRuntime = protos.ScriptRuntime +DataEncode = protos.DataEncode Group = protos.Group Key = protos.Key @@ -240,6 +262,9 @@ def corner(b, position): TouchSequence.__getitem__ = touchSequenceIndexer TouchSequence.__iter__ = touchSequenceIter +HookRpcRequest = protos.HookRpcRequest +HookRpcResponse = protos.HookRpcResponse + Bound.width = property(width) Bound.height = property(height) @@ -248,6 +273,8 @@ def corner(b, position): Bound.center = center Bound.corner = corner +Bound.__contains__ = contain +Bound.__eq__ = equal def load_proto(name): @@ -267,6 +294,13 @@ def Selector(**kwargs): return sel +class CustomOcrBackend(object): + def __init__(self, *args, **kwargs): + raise NotImplementedError + def ocr(self, image): + raise NotImplementedError + + class BaseCryptor(object): def __str__(self): return "{}".format(self.__class__.__name__) @@ -355,13 +389,14 @@ def raise_remote_exception(self, res): class ObjectUiAutomatorOpStub: - def __init__(self, stub, selector): + def __init__(self, caller, selector): """ UiAutomator 子接口,用来模拟出实例的意味 """ self._selector = selector self.selector = Selector(**selector) - self.stub = stub + self.stub = caller.stub + self.caller = caller def __str__(self): selector = ", ".join(["{}={}".format(k, v) \ for k, v in self._selector.items()]) @@ -373,7 +408,7 @@ def _child_sibling(self, name, **selector): s.setdefault("childOrSiblingSelector", []) s["childOrSiblingSelector"].append(selector) s["childOrSibling"].append(name) - return self.__class__(self.stub, s) + return self.__class__(self.caller, s) def child(self, **selector): """ 匹配选择器里面的子节点 @@ -464,6 +499,72 @@ def info_of_all_instances(self): req = protos.SelectorOnlyRequest(selector=self.selector) r = self.stub.selectorObjInfoOfAllInstances(req) return r.objects + def all_instances(self): + """ + 获取选择器选中的所有元素控件 + """ + return list(self) + def _new_object(self, **kwargs): + selector = copy.deepcopy(self._selector) + selector.update(**kwargs) + instance = self.caller(**selector) + return instance + def text(self, txt): + return self._new_object(text=txt) + def resourceId(self, name): + return self._new_object(resourceId=name) + def description(self, desc): + return self._new_object(description=desc) + def packageName(self, name): + return self._new_object(packageName=name) + def className(self, name): + return self._new_object(className=name) + def textContains(self, needle): + return self._new_object(textContains=needle) + def descriptionContains(self, needle): + return self._new_object(descriptionContains=needle) + def textStartsWith(self, needle): + return self._new_object(textStartsWith=needle) + def descriptionStartsWith(self, needle): + return self._new_object(descriptionStartsWith=needle) + def textMatches(self, match): + return self._new_object(textMatches=match) + def descriptionMatches(self, match): + return self._new_object(descriptionMatches=match) + def resourceIdMatches(self, match): + return self._new_object(resourceIdMatches=match) + def packageNameMatches(self, match): + return self._new_object(packageNameMatches=match) + def classNameMatches(self, match): + return self._new_object(classNameMatches=match) + def checkable(self, value): + return self._new_object(checkable=value) + def clickable(self, value): + return self._new_object(clickable=value) + def focusable(self, value): + return self._new_object(focusable=value) + def scrollable(self, value): + return self._new_object(scrollable=value) + def longClickable(self, value): + return self._new_object(longClickable=value) + def enabled(self, value): + return self._new_object(enabled=value) + def checked(self, value): + return self._new_object(checked=value) + def focused(self, value): + return self._new_object(focused=value) + def selected(self, value): + return self._new_object(selected=value) + def index(self, idx): + return self._new_object(index=idx) + def instance(self, idx): + return self._new_object(instance=idx) + def __iter__(self): + """ + 遍历所有符合选择器条件的元素实例 + """ + yield from [self.instance(i) for i in \ + range(self.count())] def count(self): """ 获取选择器选中控件的数量 @@ -932,7 +1033,25 @@ def wait_for_idle(self, timeout): r = self.stub.waitForIdle(protos.Integer(value=timeout)) return r.value def __call__(self, **kwargs): - return ObjectUiAutomatorOpStub(self.stub, kwargs) + return ObjectUiAutomatorOpStub(self, kwargs) + + +class AppScriptRpcInterface(object): + def __init__(self, stub, application, + name): + self.application = application + self.stub = stub + self.name = name + def __call__(self, *args): + call_args = dict() + call_args["method"] = self.name + call_args["args"] = args + req = HookRpcRequest() + req.package = self.application.applicationId + req.callinfo = json.dumps(call_args) + result = self.stub.callScript(req) + r = json.loads(result.callresult) + return r class ApplicationOpStub: @@ -1089,6 +1208,53 @@ def is_installed(self): req.user = self.user r = self.stub.isInstalled(req) return r.value + def attach_script(self, script, runtime=ScriptRuntime.RUNTIME_QJS, + emit="", + encode=DataEncode.DATA_ENCODE_NONE, + spawn=False, + standup=5): + """ + 向应用注入持久化 Hook 脚本 + """ + s = isinstance(script, str) + script = script.encode() if s else script + req = protos.HookRequest() + req.package = self.applicationId + req.script = script + req.runtime = runtime + req.standup = standup + req.spawn = spawn + req.destination = emit + req.encode = encode + r = self.stub.attachScript(req) + return r.value + def detach_script(self): + """ + 移除注入应用的 Hook 脚本 + """ + req = protos.String(value=self.applicationId) + r = self.stub.detachScript(req) + return r.value + def is_attached_script(self): + """ + 检查使用在此应用注入了 Hook 脚本 + """ + req = protos.String(value=self.applicationId) + r = self.stub.isScriptAttached(req) + return r.value + def is_script_alive(self): + """ + 检查应用中的 Hook 脚本是否正常 + """ + req = protos.String(value=self.applicationId) + r = self.stub.isScriptAlive(req) + return r.value + def __getattr__(self, name): + """ + 调用注入应用 Hook 脚本的导出方法 + """ + return AppScriptRpcInterface(self.stub, self, + name) class ApplicationStub(BaseServiceStub): @@ -1100,6 +1266,11 @@ def current_application(self): app = self.__call__(top.packageName) app.activity = top.activity return app + def get_application_by_name(self, name, user=0): + req = protos.String(value=name) + r = self.stub.getIdentifierByLabel(req) + app = self.__call__(r.value, user=user) + return app def enumerate_running_processes(self): """ 列出设备上所有正在运行的安卓应用进程 @@ -1804,12 +1975,154 @@ def save_config(self): raise NotImplementedError +class OcrOperator(object): + def __init__(self, device, elements=None, + **kwargs): + self.elements = elements + self.index = kwargs.pop("index", 0) + self.func, self.rule = kwargs.popitem() + self.match = getattr(self, self.func) + self.device = device + def text(self, item): + return self.rule == item["text"] + def textMatches(self, item): + return bool(re.match(self.rule, item["text"], + re.DOTALL)) + def textContains(self, item): + return self.rule in item["text"] + def find_target_item(self): + m = [e for e in self.elements \ + if self.match(e)] + o = (m and len(m) > self.index) != True + return None if o else m[self.index] + def find_item_or_throw(self): + item = self.find_target_item() + msg = "OcrSelector[{}={}]".format(self.func, self.rule) + item or self.throw(UiObjectNotFoundException, msg) + return item + def find_cb(self, func, ret, *args): + item = self.find_target_item() + return func(item, *args) if item else ret + def find_or_throw_cb(self, func, *args): + item = self.find_item_or_throw() + return func(item, *args) + def throw(self, exception, *args): + raise exception(*args) + def _screenshot(self, item, quality): + return self.device.screenshot(quality, + bound=item["bound"]) + def _click(self, item): + point = item["bound"].center() + return self.device.click(point) + def __str__(self): + return "Ocr: {}={}".format(self.func, self.rule) + __repr__ = __str__ + def exists(self): + """ + OCR - 检查元素是否存在 + """ + return bool(self.find_target_item()) + def exist(self): + """ + OCR - 检查元素是否存在 + """ + return self.exists() + def click(self): + """ + OCR - 点击元素(不存在则报错) + """ + return self.find_or_throw_cb(self._click) + def click_exists(self): + """ + OCR - 点击元素(不存在将不会产生异常) + """ + return self.find_cb(self._click, False) + def click_exist(self): + """ + OCR - 点击元素(不存在将不会产生异常) + """ + return self.click_exists() + def screenshot(self, quality=100): + """ + OCR - 对元素进行截图 + """ + return self.find_or_throw_cb(self._screenshot, + quality) + def take_screenshot(self, quality=100): + """ + OCR - 对元素进行截图 + """ + return self.screenshot(quality) + def info(self): + """ + OCR - 获取匹配元素的信息 + """ + item = self.find_item_or_throw() + return item + + +class OcrEngine(object): + def __init__(self, service, *args, + **kwargs): + args = list(args) + if type(service) == type: + args.insert(0, service) + service = "custom" + func = getattr(self, "init_{}".format(service)) + func(*args, **kwargs) + def init_paddleocr(self, *args, **kwargs): + from paddleocr import PaddleOCR + self._service = PaddleOCR(*args, **kwargs) + self._ocr = self.ocr_paddleocr + def init_easyocr(self, *args, **kwargs): + from easyocr import Reader + self._service = Reader(*args, **kwargs) + self._ocr = self.ocr_easyocr + def init_custom(self, service, *args, **kwargs): + self._service = service(*args, **kwargs) + self._ocr = self.ocr_custom + def ocr_custom(self, image): + result = self._service.ocr(image) + return result + def ocr_paddleocr(self, image): + r = self._service.ocr(image) + n = bool(r and r[0] and type(r[0][-1])==float) + result = (r if n else r[0]) or [] + output = [[n[0], n[1][0], n[1][1]] for n in result] + return output + def ocr_easyocr(self, image): + result = self._service.readtext(image) + return result + def ocr(self, screenshot): + img = screenshot.getvalue() + result = self._ocr(img) or [] + output = [self.format(*n) for n in result] + return output + def format(self, box, text, confidence): + bound = Bound() + bound.left = int(min(p[0] for p in box)) + bound.top = int(min(p[1] for p in box)) + bound.bottom = int(max(p[1] for p in box)) + bound.right = int(max(p[0] for p in box)) + info = dict(text=text, confidence=confidence, + bound=bound) + return info + + class Device(object): def __init__(self, host, port=65000, certificate=None, session=None): self.certificate = certificate self.server = "{0}:{1}".format(host, port) + policy = dict() + policy["maxAttempts"] = 5 + policy["retryableStatusCodes"] = ["UNAVAILABLE"] + policy["backoffMultiplier"] = 2 + policy["initialBackoff"] = "0.5s" + policy["maxBackoff"] = "15s" + config = json.dumps(dict(methodConfig=[{"name": [{}], + "retryPolicy": policy,}])) if certificate is not None: with open(certificate, "rb") as fd: key, crt, ca = self._parse_certdata(fd.read()) @@ -1819,14 +2132,20 @@ def __init__(self, host, port=65000, self._chan = grpc.secure_channel(self.server, creds, options=(("grpc.ssl_target_name_override", self._parse_cname(crt)), + ("grpc.service_config", config), ("grpc.enable_http_proxy", 0))) else: - self._chan = grpc.insecure_channel(self.server) + self._chan = grpc.insecure_channel(self.server, + options=(("grpc.service_config", config), + ("grpc.enable_http_proxy", 0)) + ) session = session or uuid.uuid4().hex interceptors = [ClientSessionMetadataInterceptor(session), GrpcRemoteExceptionInterceptor(), ClientLoggingInterceptor()] + self._ocr = None + self._ocr_img_quality = 75 self.channel = grpc.intercept_channel(self._chan, *interceptors) self.session = session @@ -1834,6 +2153,14 @@ def __init__(self, host, port=65000, def frida(self): if _frida_dma is None: raise ModuleNotFoundError("frida") + try: + device = _frida_dma.get_device_matching( + lambda d: d.name==self.server) + # make a call to check server connectivity + device.query_system_parameters() + return device + except: + """ No-op """ kwargs = {} if self.certificate is not None: kwargs["certificate"] = self.certificate @@ -1897,6 +2224,8 @@ def get_last_activities(self, count=3): return self.stub("Application").get_last_activities(count=count) def start_activity(self, **activity): return self.stub("Application").start_activity(**activity) + def get_application_by_name(self, name): + return self.stub("Application").get_application_by_name(name) def application(self, applicationId, user=0): return self.stub("Application")(applicationId, user=user) # 快速调用: Util @@ -2045,6 +2374,26 @@ def device_info(self): return self.stub("UiAutomator").device_info() def __call__(self, **kwargs): return self.stub("UiAutomator")(**kwargs) + # OCR 功能扩展 + def ocr(self, index=0, **kwargs): + if not isinstance(self._ocr, OcrEngine): + raise IllegalStateException("Ocr engine is not setted up") + if any(r not in ["text", "textContains", "textMatches"] \ + for r in kwargs.keys()): + raise InvalidArgumentError("Only text* matches are supported") + if len(kwargs) != 1: + raise InvalidArgumentError("Only or at least one rule can be used") + image = self.screenshot(self._ocr_img_quality) + return OcrOperator(self, + elements=self._ocr.ocr(image), + index=index, + **kwargs + ) + def setup_ocr_backend(self, service, *args, quality=75, + **kwargs): + self._ocr_img_quality = quality + self._ocr = OcrEngine(service, *args, + **kwargs) # 日志打印 def set_debug_log_enabled(self, enable): level = logging.DEBUG if enable else logging.WARN diff --git a/lamda/rpc/application.proto b/lamda/rpc/application.proto index 3fdedc3..2fce24d 100644 --- a/lamda/rpc/application.proto +++ b/lamda/rpc/application.proto @@ -13,6 +13,16 @@ enum GrantType { GRANT_IGNORE = 2; } +enum DataEncode { + DATA_ENCODE_NONE = 0; + DATA_ENCODE_ZLIB = 1; +} + +enum ScriptRuntime { + RUNTIME_QJS = 0; + RUNTIME_V8 = 1; +} + message ApplicationRequest { string name = 1; string permission = 2; @@ -83,3 +93,23 @@ message ApplicationProcesses { message ApplicationPkgNames { repeated string names = 1; } + +message HookRequest { + string package = 1; + bytes script = 2; + ScriptRuntime runtime = 3; + string destination = 4; + DataEncode encode = 5; + uint32 standup = 6; + bool spawn = 7; +} + +message HookRpcRequest { + string package = 1; + string callinfo = 2; +} + +message HookRpcResponse { + string package = 1; + string callresult = 2; +} \ No newline at end of file diff --git a/lamda/rpc/services.proto b/lamda/rpc/services.proto index 8317d36..d534ed3 100644 --- a/lamda/rpc/services.proto +++ b/lamda/rpc/services.proto @@ -49,6 +49,13 @@ service Application { rpc addToDozeModeWhiteList(ApplicationRequest) returns (Boolean) {} rpc removeFromDozeModeWhiteList(ApplicationRequest) returns (Boolean) {} + rpc getIdentifierByLabel(String) returns (String) {} + + rpc callScript(HookRpcRequest) returns (HookRpcResponse) {} + rpc isScriptAlive(String) returns (Boolean) {} + rpc isScriptAttached(String) returns (Boolean) {} + rpc attachScript(HookRequest) returns (Boolean) {} + rpc detachScript(String) returns (Boolean) {} } service Debug { diff --git a/properties.local.example b/properties.local.example index dc56cd9..2957e04 100644 --- a/properties.local.example +++ b/properties.local.example @@ -1,125 +1,151 @@ -# properties.local 配置文件 +# properties.local configuration file # -# ------ 请勿直接复制此文件 请挑选所需配置新建文件 ------ -# -# 注意请务必确保该文件的属主以及权限为 root:root, 600 -# 此文件是 LAMDA 的默认配置文件,你可以在启动时让 LAMDA 自动启动一些服务 -# 文件应该位于 /data 或者 /data/usr (LAMDA 首次启动前不会存在该目录,如不存在请自行创建) -# 它的读取顺序为以上列出的顺序 -# -# 使用 # 开头的行为注释行,将被忽略 -# 请确保每行的格式均为 a=b 且不得包含空格 -# -# 如果文件中存在重复的配置项,最后一个重复的配置项将被使用,例如存在两行配置 -# openvpn.enable=false -# openvpn.enable=true -# 那么,最后一个 true 将被使用 +# DO NOT DIRECTLY COPY THIS FILE, PLEASE SELECT THE NECESSARY CONFIGURATION LINES TO CREATE A NEW FILE # +# This file is the LAMDA service configuration file. You can use +# it to automatically start some services when LAMDA starts. +# The file should be located in /data or /data/usr (this directory +# will not exist before the first startup, please create it if it does not exist). +# Lines starting with # and ; are comment lines and will be ignored. +# Please ensure that each line follows the format a=b and does not contain spaces. +# If there are duplicate configuration items across multiple files, +# the last duplicate will be used. -# 设置 LAMDA 的默认监听端口 +# Set the default listening port for LAMDA. port=65000 -# 你可以将远程桌面上显示的 LAMDA 改成任意你想要的名称(至多10字符) -brandname=LAMDA +# You can change the name displayed on the remote desktop for +# LAMDA to any name you prefer (up to 10 characters). +brandname=FIRERPA -# 设置 LAMDA 的远程桌面登录密码,6-32 位字符 -# 注意:此密码 仅在使用了证书 (--certificate) 的情况下才会生效 -# 因为如果不使用证书加密远程桌面以及API及其他流量,设置密码也为徒劳 -# 此密码用于作为证书中生成密码的备用密码(便于记忆) +# Set the LAMDA certificate. It will encrypt your remote desktop +# and API traffic, and enable password authentication. You can obtain +# the following configuration value by running 'base64 -w0 lamda.pem'. +# It is the base64-encoded certificate. +cert=TEFNREEgU1NMIENFUlRJRklDQVRFIChDTj10ZXN0LFBBU1NXRD1hMWMwZTNlYTcwN2E1NGRlN2EwZjk1KQotLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS0KTUlJRXZnSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2d3Z2dTa0FnRUFBb0lCQVFEVmNMWlA5b0xRWkRIRgp5V0pTa2U0Z0crSUpmSCtMWlk0cXUzdS9OckRwSHZCN2k5V01rMWxRL2FMSGI0V3ZqelBLK1RITm9rRzc2MENRClhBTUpWS0dmYXRwcmNLUXdvMGhvWDZ2NlhsTVlZUlNRbW9wN3pSaUtnN3ZxKzV2S09DQ2RKcDFlSVNiZXcyTEgKTmYxL3JZelpwa1Q1bHoxTGZkem00eXJBS3VNa0tyZ3pnTzJRcE9CQVdYdmdiWG9BMDdidDdOODZZOGdNZFUwdAp6Ui9EcmhLTi9JMVdYSk11MU4wQW5UbDJRdEhEb0dCN0UyS0xpdmwybDZJdnRrYWJ4RE55Y2lHbUxOUGlMRklrCllPSHlnMUg3MUJ2NU04NE5TWDc1c2xuOXVNUGUzOVNFVlJoU0ptNHcvT2tXZnA5dGpZRUx3dHphdFhoSWJoS3MKaC9OZU1lOWRBZ01CQUFFQ2dnRUFCYlFuWUdlcFdKYjAydURtSnhLNGx2OFNhL0o1dzJpSldMYjk0dW1SUExRKwpTa1E5c1Zpc0JiQU1JNHc2dWFyNEFBVTh3WGxaTndPekMvM2V6dWNHRXFreEFReXM0VDB4SXRPUkF2WExxVlowCkl1WnpxNW53Si9OeFFzeEtmaWhBZkRLYlRmZjdmcG5MWlV0dlpNbG5LWUhQVExtRlFua3drckwyRE5YdDVVOTYKYXJUUDVOY0x1aDQ2dU93alJUOWhaNytYQi9ubU9LeTV4V3hoNWVMQmJJUTJrS0UvdWViUlVYRGZNdG5tbTh2aApRSG9VM3N5dzlZcE1xRDlWQWppcGlqQXRwbUlwa2w1emRWSE52Q1dCSGk2NjZxKzF6cUpHeFVUODBseHo1N2R1ClRvRFFQc1l0OFFEL3ZjNGkxajd0bDZyRzNQWkJNM05LNVR5ZFYyRnlnUUtCZ1FEYUcycnV3aWxyYUdZRzZNQWwKNEF2WW1BY0hHQWUwQjR0ZmtkdS9QandlRWQyWUF2TjJIQWV1Z1ZUSWg1eFplUlIwNFE4ZVNGenBTaEpwREpkNAp1TEhHcXJ2cmpXL1greEVIc081NnNjNjRiem1weWJWQXBlQnA4NlFGSTk4V3FmZkFyN3FzbzhweFJjNmdTRU1uCk5TcXV1Z2psYU05TlJmcUo4ci82RklmQkxRS0JnUUQ2aGJ0Z2dIdVJoTHhHYWNNaERvOVppcXJlNi9HN1dvZnQKR2FGZmFQM2xZNTNmM0hPdSsyNTZMWDY1ZWFYRTdrbXlQOGRDM2VWL24xT3dvTHdBYm1BZ2pEWVd1N09KdGk4QQpPbG9VNmtnTkVwNWcwOHpVWlBaR3NSMkZMd2VpUkgrQ3ZOdFBZakJydmFIQUtVU2lLa3BKdEpWeFpIdUl6SlpGCjVUZkM2VjNrOFFLQmdEeWp0TlpPKzA4V2hvOVROT0VTNnBnOHBHK1BlY3pPOEN3UkZJU1dYQWFNTnd6bGZTVVEKWS81YmpPUDMrRHRVRTZEdlZkRzRrc1IxeUtxV1NxTFF6dlNLVVpjTEN0YUV3bFplRmQvZEFibDdpdyt1dWdzUQpVMVdCM005bENzaDFWeUdtZWdNM3dyZzlqVlk0NFJyTWlHSnQ3TDFEcDZjM1ZwSDJBUFFac3lpOUFvR0JBTk9pCmpmeWtEYitNNXBDRllEWlkybmpHVURzcUQzZzZyb0Y2R1gxRWNOaU1JeDZ1V1h3RkkvdEsyN2RNTU9JQWUzbDkKcjVPcGFPczdhYlBZMVhsM3hQVTUvYWVPd2NrZ2d1d3FYMWN6NDlKSFhFeG9JSzE4N1NBakY5RWZQYyt6RmhVWAovaDA5MGJIeTdPWXM5cklZRDlIY0lETStzNjJKUjVtY1hsTG1Xay9CQW9HQkFKRVhQV05IWEwra1l6My91R1c3CnRKd0hUQzFlbEJjclcvaHpJMWt4ZEhXem5VaXNTWlcyVnA5b0wwSWNrQXVWQkx6eGUvR1h6OGJRTjZkOWwyZDAKdGtmUmo1TmpDOTUzS2N1cTNSekRmVU40cTcyUlVWTWlFOHVvSTBkVVZpalczN0tVMEhLcm1pbDBocU01eW9iNQpVZlhPQ2Q5SlRRSWx5Y2dNWER6Tm00S3oKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ3FEQ0NBWkNnQXdJQkFnSVJBUHNjMVBRNXBuSDNhNk1GZkdVTXA2WXdEUVlKS29aSWh2Y05BUUVMQlFBdwpFREVPTUF3R0ExVUVDZ3dGVEVGTlJFRXdIaGNOTWpBd01UQXhNREF3TURBeFdoY05Namt4TWpJNU1EQXdNREF4CldqQVBNUTB3Q3dZRFZRUUREQVIwWlhOME1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0MKQVFFQTFYQzJUL2FDMEdReHhjbGlVcEh1SUJ2aUNYeC9pMldPS3J0N3Z6YXc2Ujd3ZTR2VmpKTlpVUDJpeDIrRgpyNDh6eXZreHphSkJ1K3RBa0Z3RENWU2huMnJhYTNDa01LTklhRityK2w1VEdHRVVrSnFLZTgwWWlvTzc2dnViCnlqZ2duU2FkWGlFbTNzTml4elg5ZjYyTTJhWkUrWmM5UzMzYzV1TXF3Q3JqSkNxNE00RHRrS1RnUUZsNzRHMTYKQU5PMjdlemZPbVBJREhWTkxjMGZ3NjRTamZ5TlZseVRMdFRkQUowNWRrTFJ3NkJnZXhOaWk0cjVkcGVpTDdaRwptOFF6Y25JaHBpelQ0aXhTSkdEaDhvTlIrOVFiK1RQT0RVbCsrYkpaL2JqRDN0L1VoRlVZVWladU1QenBGbjZmCmJZMkJDOExjMnJWNFNHNFNySWZ6WGpIdlhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBWEQ0L1cKQjBhSW1aWGpQbTRxUnBOazJmUnpjU1g4MGw2TlZaWWxJV3ZYalFxUXdXZnMvSGczZDVzYUpickFmcWVPa1lQdQpjeXJEWFZPdC9RTEVDOTFBSGtjRWJ1R0dPMGNFU2YyOHdUM1UzRnJJb2cxS1VyTURqWFFIb09vZEJpOGdNaVBmCmROcWhMSTdkNDJBTXJKU3dZUTlSUG9vWG9UZ2xDa0d3R291RDhuS0V5MmNHeVMxM3lQcDRseC9TWTR1QkRFU0sKRlErR0ZRTExGQktQZHZNc2x0cHYyQWFMWmR3clF4aFQ2aTU1U1puNStLb3c1TGxYL0RHdUw5UnRPdmZ2T0tzZQpRZ3pOQUg3QkYzbGdvQmJjYk9yZkVQazY1ZEZRN0NXYi91aDZjVmlmSjdxQzkvL0xhdElmb1VQVnJiRXdZL2dRCk5BRXFYclduMGZuYUc0cUEKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2xEQ0NBWHdDQVFBd0RRWUpLb1pJaHZjTkFRRUxCUUF3RURFT01Bd0dBMVVFQ2d3RlRFRk5SRUV3SGhjTgpNakF3TVRBeE1EQXdNREF4V2hjTk1qa3hNakk1TURBd01EQXhXakFRTVE0d0RBWURWUVFLREFWTVFVMUVRVENDCkFTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTG5xZkJadnJHWmFxZ2s1bXNDUlJwUHoKcC8rNDY0akJrbmxtVEtldE9ja0RUVXE4VjZmSC8yR2ZiNkhqam9ENXBrQ3RENW1TS2thSE5odXhMWHNGZkVmYwpLbG1ubjNacGp5Tk9IRUEvaUFPMkR5RVlhMDh4U2V2TTdXb2piRjdjTmo1L0RZZzdlYjBpMCsvL2JCbGg4bmxPCmdoU1VoQ1RNNVBDb2ZMRFU4c1ZYdVlBaUdVNlV6QnJJQzB2SEVsdERraUpWTHBjQ3RzS2pFWk9za1BkQWM3dTYKL2FBMFA1R29uWjVVa1JEWXBhK2plSlVhYnFXWlFRRWd0bXZqbG1VVWlYd3UwalJuajFuMFQzZlBRRDNnQStMSQp2QUU5dmd2cFk1WFFqNm90cEJ2c1ozTUpKTktjVU1RdTF6T0FOVHpPMThUbEE4S29CTnNCeThaOURRWktYRjhDCkF3RUFBVEFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBZUduL055cUlSSS8wQUdxdkhQOTdLdFE3NlRqNmFjaGIKMzBMSVhXcCtZSFVhTWVBVmpkMlo3alNRcDVtWlpGbCtrMWZiMzM3SWVhR1hvZlZJMjFlSzUyUVgydGVOb0JrQQovVi9PMUh1MzUvK2FpejB4c2RENndXdndvNEZ5MWpsbWFlSmh3ZFFhY0JsREdGQTJqRkp4dUVwYWhmeFp2VXNiCjNqNXpVMFdLVFVDZkVEZ1hGd0J3MTJ4a3UvN1RNZENFYlJzWWFaM3pGVEMyMjZsUWJVRE43d2VxRndTRCt0QjYKUnVoSXhlOCtjRndBc0FXSENsZXJLZ1pucjN0NVFGMDc4cFcyR0h5OENzSjdWM01aVDVsWjQzbFM1TklCOUp6WgpTWXhaL2l6aFJ5aDVxUjczdUFnc0phTDU2QmorY1Fxbm9UcWhMWlZsN0orTTZXaFdLem9qc0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + +# Set the remote desktop login password for LAMDA, 6-32 characters. +# This password will only take effect if a certificate (--certificate) +# is used. It serves as a backup password to the one generated in +# the certificate (for easier memorization). ssl-web-credential=password123 -# 设置此项将同时导致 LAMDA 的 Python 客户端 API 停用 -# 此项在且仅在启动 LAMDA 后设备崩溃,或者你不需要使用 API 时设置 -# 此选项同时将禁用包括任何可能对系统稳定性产生影响的组件 +# Setting this option will also disable the LAMDA Python client API. +# This option should be set only if the device crashes after starting LAMDA, +# or if you do not need to use the API. This option will also disable +# any components that might impact system stability. disable-client-api=false -# 增强隐身模式,在可能损坏自身或第三方组件部分 -# 功能的情况下尽量保护 LAMDA 及其附属组件不被探测 -# 非必要请勿使用 (7.65 版本引入) +# Enhanced stealth mode, designed to protect LAMDA and its associated +# components from detection in cases where there is a risk of damaging +# itself or third-party components. +# Use only if necessary (introduced in version 7.65). enhanced-stealth-mode=false -# ---------- OpenVPN 配置 ---------- -# ***** 注意请勿手动编写此 OpenVPN 配置,你应该始终使用 tools -# ***** 目录下的 openvpn 镜像中提供的脚本来生成此配置 -# ***** 并做适当修改,以下是一个示例配置 +# Remote Desktop Touch Fix +# If there is an offset or no response when you touch the screen on +# the remote desktop, please set the following configuration: +touch.backend=system + +# ---------- OpenVPN Configuration ---------- +# Do not manually write the following configuration. You should use +# our accompanying OpenVPN server setup solution to set it up and +# generate this configuration using its built-in commands. openvpn.proto=udp openvpn.cipher=AES-256-GCM openvpn.host=123.123.123.123 openvpn.port=1190 -openvpn.ca=LS0tLS1CRUdJTiBDRVJUSUZJQ0FUR... -openvpn.cert=LS0tLS1CRUdJTiBDRVJUSUZJQ0FUR... -openvpn.key=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS... -openvpn.tls_encryption=tls-crypt +openvpn.ca=LS0tLS1CRU... +openvpn.cert=LS0tLS1CRU... +openvpn.key=LS0tLS1CRU... +openvpn.tls_encryption= openvpn.tls_key_direction= -openvpn.tls_key=LS0tLS1CRUdJTiBP... -# ---- 以下为 OpenVPN 可配置项 ---- -# 是否开启全局 VPN +openvpn.tls_key= +# ONLY THE FOLLOWING CONFIGURATION ITEMS CAN BE MANUALLY CONFIGURED. +# Whether to enable global VPN. openvpn.global=false -# 是否启用 true | false +# Whether to enable the service true | false openvpn.enable=true -# ---------- Gproxy 配置 ---------- -# 此配置用来使 LAMDA 在启动时自动连接到代理 +# ---------- Proxy Configuration ---------- +# This configuration is used to make LAMDA automatically connect +# to the proxy server at startup. # -# 是否启用 true | false +# Whether to enable the service true | false gproxy.enable=true -# 代理类型 http-connect 或者 socks5 +# The proxy type can be either http-connect or socks5. gproxy.type=http-connect -# 代理服务器地址 +# Proxy server address gproxy.host=172.1.1.1 -# 代理服务器端口 +# Proxy server port gproxy.port=8080 -# 代理服务器的登录密码(置空则无认证) +# Proxy server login password (leave empty for no authentication) gproxy.password= -# 代理服务器的登录用户(置空则无认证) +# Proxy server login username (leave empty for no authentication) gproxy.login= -# ---------- CRONTAB 服务 ---------- -# 如果你不需要用到定时任务,可以关闭此服务 -# 是否启用 true | false +# ---------- CRONTAB Scheduled Task Service ---------- +# If you do not need scheduled tasks, you can disable this service. +# Whether to enable the service true | false cron.enable=true -# ---------- SSHD 服务 ---------- -# 如果你不需要用 ssh 连接设备,可以关闭此服务 -# 是否启用 true | false +# ---------- SSHD Service ---------- +# If you do not need to connect to the device via SSH, you can disable this service. +# Whether to enable the service true | false sshd.enable=true -# ---------- 端口转发(frp) 服务 ---------- -# 是否启用 true | false +# ---------- Port Forwarding (frp) Service ---------- +# Whether to enable the service true | false fwd.enable=true -# 转发到远端的端口(0则代表任意分配) +# Port to forward to the remote (0 means randomly allocated) fwd.rport=0 -# frp 服务器的地址 +# FRP server address fwd.host=123.123.123.123 -# frp 服务器端口 +# FRP server port fwd.port=9911 -# frp 协议 +# FRP protocol fwd.protocol=tcp -# frp 登录认证 +# FRP login authentication fwd.token=abc123 -# ---------- ADB 服务 ---------- -# 是否启用 true | false +# ---------- ADB Service ---------- +# Whether to enable the service true | false adb.enable=true -# 内置 adb 的默认目录 (adb shell 目录) +# Default working directory for built-in ADB (adb shell working directory) adb.directory=/data/local/tmp -# 如果为 true,那么 adb 连接将拥有 root 身份,否则为 shell 身份 -# 当此项配置为 false,你也将用于类原生的 adb shell,你将无法使用 LAMDA 内置命令 -# 同时请注意,由于 adb 并无 TLS 连接,流量可能会被监听,所以为了安全起见 -# 当 LAMDA 服务使用了certificate 启动时,默认情况下该值将自动设为 false -# 但当你将其写入 properties.local,则始终以文件中的配置为准 -# 请自行对安全性负责 +# If set to true, the ADB connection will have root privileges, +# otherwise it will have shell privileges. When this option is set to false, +# you will be using a native-like adb shell, and you will not be able to use +# LAMDA's built-in commands. Please note that since ADB does not use TLS connections, +# traffic may be monitored. For security reasons, when the LAMDA service is +# started with a certificate, this value will be set to false by default. +# However, if you specify it in the properties.local file, the configuration +# in the file will take precedence. +# You are responsible for ensuring security. adb.privileged=true -# ---------- 代理服务 ---------- -# 是否启用 true | false +# ---------- Bridge Proxy Service ---------- +# Whether to enable the service true | false tunnel2.enable=true -# 只有 login 以及 password 全部设置,代理才会需要登录验证,任一项留空代理将无需登录验证 +# The bridge proxy will require login authentication only if both login and password +# are set. If either one is left empty, no login authentication will be required. tunnel2.login=lamda -# 代理的登录密码 +# Login password for the bridge proxy tunnel2.password=1234 -# 出网接口 (rmnet|wlan) -# 当出网接口为 rmnet 时,代理将尝试通过移动数据转发你的请求 -# 当出网接口为 wlan 时,将通过 wlan 接口转发请求 -# 当配置为空时,将使用默认网络转发请求 -tunnel2.iface=rmnet \ No newline at end of file +# Outbound interface (rmnet|wlan) +# When the outbound interface is rmnet, the proxy will attempt to forward your requests through mobile data. +# When the outbound interface is wlan, requests will be forwarded through the wlan interface. +# If the configuration is empty, the default network will be used to forward the requests. +tunnel2.iface=rmnet + +# ---------- mDNS Broadcast Service ---------- +# Enable or disable true | false +mdns.enable=true +# Add TXT metadata for mDNS. When enabled, it will support querying device information +# such as model, ABI, and device ID using tools like python-zeroconf. Disabled by default. +mdns.meta=false +# Set the broadcast domain name using a locally unique ID, default is {DEVICEID-UNIQUE}.lamda. +# If the name duplicates in the local network, a suffix ID will be automatically added. +mdns.name=DEVICEID-UNIQUE.lamda +# Set the broadcast service name, default is lamda, i.e., _lamda._tcp.local. +mdns.service=lamda diff --git a/setup.py b/setup.py index 3ab5ef8..d550572 100644 --- a/setup.py +++ b/setup.py @@ -7,10 +7,10 @@ setuptools.setup( name = "lamda", version = "{}".format(__version__), - description = "Android reverse engineering & automation framework", - url = "https://github.com/rev1si0n/lamda", + description = "Android reverse engineering & automation framework (Client API)", + url = "https://github.com/firerpa/lamda", author = "rev1si0n", - python_requires = ">=3.6,<=3.12", + python_requires = ">=3.6,<3.13", zip_safe = False, extras_require = { "full": ["frida>=16.0.0,<17.0.0"], @@ -19,9 +19,9 @@ ], }, install_requires= [ - "grpcio-tools>=1.35.0,<1.60.0", - "grpc-interceptor>=0.13.0,<=0.15.2", - "grpcio>=1.35.0,<1.60.0", + "grpcio-tools>=1.35.0,<=1.68.0", + "grpc-interceptor>=0.13.0,<=0.15.4", + "grpcio>=1.35.0,<=1.68.0", "cryptography>=35.0.0", "msgpack>=1.0.0", "asn1crypto>=1.0.0,<2", diff --git a/tools/README.md b/tools/README.md index 816a2ab..8edb5cb 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1 +1 @@ -使用方法请查看项目 WIKI \ No newline at end of file +使用方法请查看 https://device-farm.com/doc/ \ No newline at end of file diff --git a/tools/fridarpc.py b/tools/fridarpc.py index 9b0c548..97791a4 100755 --- a/tools/fridarpc.py +++ b/tools/fridarpc.py @@ -17,7 +17,7 @@ help="service port") parser.add_argument("-f", type=argparse.FileType("r"), dest="script", help="frida script", required=True) - parser.add_argument("-delay", type=int, dest="delay", default=1.5, + parser.add_argument("-delay", type=int, dest="delay", default=5, help="attach after delay") parser.add_argument("-cert", type=str, default=cert, help="ssl cert") @@ -26,17 +26,8 @@ d = Device(args.device, port=args.port, certificate=args.cert) - pid = d.frida.spawn(args.package) - d.frida.resume(pid) - - time.sleep(args.delay) - session = d.frida.attach(pid) - session.on("detached", print) - - sc = session.create_script(args.script.read()) - - sc.on("destroyed", print) - sc.on("message", print) - sc.load() - sc.eternalize() + app = d.application(args.package) + app.detach_script() + app.attach_script(args.script.read(), + standup=args.delay) exit (0) \ No newline at end of file diff --git a/tools/requirements.txt b/tools/requirements.txt index fd606f4..47f74e0 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,5 +1,5 @@ mitmproxy>=9.0.0,<=10.2.0 dnspython -httpx[socks] +httpx[socks]<0.28.0 packaging Pillow \ No newline at end of file diff --git a/tools/startmitm.py b/tools/startmitm.py index e8c7622..af15958 100755 --- a/tools/startmitm.py +++ b/tools/startmitm.py @@ -172,7 +172,7 @@ def get_default_interface_ip(target): print (r" /____ > |__| (____ /__| |__| |__|_| /__||__| |__|_| / ") print (r" \/ \/ \/ \/ ") print (r" Android HTTP Traffic Capture ") -print (r"%60s" % ("lamda#v%s BY rev1si0n" % (__version__))) +print (r"%60s" % ("lamda#v%s BY firerpa" % (__version__))) pkgName = None diff --git a/tools/test-fridarpc.js b/tools/test-fridarpc.js index d7706b2..bac73ad 100644 --- a/tools/test-fridarpc.js +++ b/tools/test-fridarpc.js @@ -3,11 +3,27 @@ Java.perform(function() { rpc.exports = { getMyString: function (paramA, paramB) { return performRpcJVMCall(function() { + // 可以使用 Frida java 相关功能,Java.use 等 var newParam = String.$new("helloWorld").toString() return newParam + ":" + paramA + paramB }) - } + }, + getMyString1: function (paramA, paramB) { + return performRpcJVMCallOnMain(function() { + // 可以使用 Frida java 相关功能,Java.use 等 + // 执行于应用的主进程,适用于涉及到 UI 主线程相关的功能 + var newParam = String.$new("helloWorld").toString() + return newParam + ":" + paramA + paramB + }) + }, + getMyString2: function (paramA, paramB) { + return performRpcCall(function() { + // 这里不能使用 Java 相关功能 + return paramA + paramB + }) + }, } -createFridaRpc("myRpcName", rpc.exports) +// 创建名为 myRpcName 的调用接口 +createRpcEndpoint("myRpcName", rpc.exports) console.log("fridarpc test loaded") });