• 20199324 2019-2020-2 《网络攻防实践》综合实践


    从四大安全会议挑选1篇论文,阅读并重现该论文的工作,如果能够改进可加分。具体要求如下:
    ①从整体上介绍下论文的研究内容、论文的出处和作者、论文的研究方法、优缺点等。
    ②具体介绍论文的研究工作。
    ③通过分析该论文,请列出来与论文相关的值得研究的方向并详细介绍如何研究,给出研究思路
    ④要求提供所读论文的电子版、综合实践报告、制作PPT讲解分析、录制视频、论文源代码(必须项)
    ⑤评分根据工作量、内容分析的深刻程度、PPT和视频的质量、报告的质量等综合评定。

    论文阅读和分析

    介绍

    • Android在恶意软件感染方面位居操作系统之首。部分原因是:android生态系统的开放性。允许用户安装未经验证来源的apps(也就是用户可以从第三方应用程序商店安装apps,而无需进行人工检查或完整性破坏)。使得恶意软件易于传播。
    • app-web interface:恶意软件传播的载体是良性合法的apps,它将用户引向访问托管恶意应用程序的网站,本文称之为App-Web Interfaces。
    • 恶意链接的访问:
      • 通过直接嵌入apps中的web链接发生;
      • 通过来自广告网络的广告登录页面(landing pages)访问。
    • 分析恶意软件传播媒介的解决方案包含的三个组件:(分析框架)
      • 触发(或者浏览)应用程序UI、追踪任何可以访问的web链接;
      • 检测恶意内容;
      • 收集来源信息(即恶意内容如何到达)
    • 该分析框架可以探索从app访问的web链接,检测任何恶意活动,通过自动运行apps的用户界面,访问和记录触发的任何web链接来动态分析apps。【该系统可以自动分析app-web interface,无需人工干预即可连续运行
    • 用该框架分析了600,000个应用程序,收集了大约150万个URL,然后使用已建立的URL黑名单和防病毒软件对它们进行了下一步分析,以识别恶意网站和可从此类网站上下载的apps。【该方法可以探索从移动应用程序内部访问的web,这是传统的搜索引擎和网站黑名单系统(如谷歌安全浏览)还没有做到的】
    • 优点:
      • 该分析框架可以探索从app访问的web链接,检测任何恶意活动,通过自动运行apps的用户界面,访问和记录触发的任何web链接来动态分析apps;
      • 该系统可以自动分析app-web interface,无需人工干预即可连续运行;
      • 整个系统有适当的重试和超时机制,可以运行数月,而不需要大量的人力;
      • 应用程序在虚拟化环境中最多运行五分钟,平均运行时间不到两分钟。
    • 缺点:
      • 该方法基于动态分析,可能无法现实应用程序中的所有链接和广告;
      • 由于广告客户是通展示次数或者点击次数为广告付费,因此该分析可能会导致经济干扰。

    背景

    1. android Ecosystem

    • android是主要的移动操作系统,核心操作系统由Google开发。
    • 处理系统应用程序外,还允许运行第三方apps(可以丰富用户设备的功能)。
    • Google play:是Google维护官方的android应用程序商店,是用户查找和安装应用程序的主要场所。
    • 在中国,非官方商店是下载apps的主要方法。
    • 用户还可以通过其他apps中的广告来发现其他apps。这些广告可以通过广告网络提供,也可以由开发人员直接嵌入,而无需中介广告网络参与。
    • 在某些情况下,apps中可能包括直接web links(即不属于任何app store)

    2. advertising

    • 应用内广告是应用程序开发人员的重要收入来源。

    • 广告商(advertisers):希望对其产品做广告的各方。

    • 发布者(publisher):是给用户带来广告的移动应用(或开发者)。

    • 广告网络或聚合器(Ad networks or aggregators):将发布者与广告商联系起来,由广告商向发布者支付费用。

    • 重定向链(redirection chain):指网页重定向中的URL

    • 登录页面(landing page):指重定向到的最终的网页

    • 广告库(ad library):带有广告的apps中嵌入了广告网络的一些代码,该代码提供了广告网络和发布者(开发人员)之间的纽带,负责管理和投放广告,成为广告库。

    3. android malware

    • android容易受到恶意软件攻击的部分原因:android生态系统的开放性。即app可以通过web连接和非官方的应用程序商店下载,用户无法知道下载的apps是否受到信任。
    • 大部分android恶意软件都是木马程序(具有所谓的有用功能和隐藏恶意功能的apps)
    • android实现了sandboxed功能,因此一个app的受害不意味着整个系统受害。【sandboxed:原理是通过重定向技术,把程序生成和修改的文件定向到自身文件夹中,当某个程序试图发挥作用,安全软件可以让他在沙盒中运行,如果有恶意行为,则禁止程序进一步运行,不会对系统造成危害。】

    方法

    1. Triggering App-Web interfaces

    • 与应用程序进行交互来启动web链接(可以静态嵌入在app中,也可以动态生成,例如广告)
    • 为了从应用程序内部触发网页链接,在自定义动态分析环境中运行apps,由于要实现可伸缩性和连续操作,需要在Android模拟器中运行。
    • 需要实现在GUI中自动导航来触发App-Web interfaces
    1.1 Application UI Exploration
    • UI:user interface(用户界面)
    • app UI 界面的探索需要触发App-Web界面
    • 一个有效的用户界面探测器需要提供高覆盖率(用户界面,这也可以转化为代码覆盖率),同时避免冗余的探索。
    • 该文使用了之前在AppsPlayground中的探索式方法和算法。
    • UI Exploration(用户界面探索)通常包括从显示的用户界面中提取特征(小部件层次),并迭代地构建应用程序的用户界面组织的模型或状态机——即不同的窗口和小部件是如何连接在一起的。
    • 黑盒(或灰盒)技术(如AppsPlayground)可以应用试探法来识别哪些窗口和小部件是相同的,以防止这些元素的冗余探索。
    1.2 Handling Webviews
    • 在研究广告时候面临的巨大的挑战是:大多数的应用内广告的实现都是在自定义的Webview(是呈现网络内容的特殊小部件,例如,HTML、JavaScript和CSS)上实现的。在从系统获得的UI层次结构中,Webview和一些自定义小部件是不透明的,也就是说,在它们内部呈现的用户界面在本机(native)用户界面层次结构中无法观察到,因此与它们的交互将受到限制。目前为止的研究没有对这个问题提出令人满意的解决方案。
    • 开源项目Selendroid可以用于获取有关webview内部的某些信息,本文作者围绕Selendroid开发了与webview交互的代码,但很难使用webview提供的信息来触发广告,
    • 为解决webview带来的问题(即android提供的UI调试界面没有分层结构):采用计算机图形技术来检测按钮和小部件,就像我们可以看到这些按钮和部件一样。
    • 按钮检测算法
      • 对视图图像执行边缘检测:通过检测图像亮度的不连续来工作。使用Canny边缘监测算法(一种经典、性能良好的边缘监测算法);
      • 在图像中查找轮廓:使用计算的边缘计算图像的轮廓,以获得对象边界
      • 忽略非凸轮廓或非常小的轮廓区域:由于按钮通常具有凸起的形状和足够大的面积,因此用户可以容易地点击它们,我们忽略非凸起的轮廓和那些在阈值参数内具有小面积的轮廓。在该步骤中,消除了许多轮廓,例如那些由文本产生的轮廓、或者嵌入图像中的非凸轮廓、或开放轮廓。
      • 计算所有剩余轮廓的边界框:对于剩余的轮廓,我们计算边界框,或者包含这些轮廓的最小矩形。这一步只是简单地确定一个中心点,在这个点上可以点击来模拟按钮点击。
    • 该技术只依赖于计算机图形算法,完全是黑盒,因为它甚至不需要从系统中提取用户界面层次。因此,它通常可用于其内部对用户界面层次提取不透明的任何窗口小部件。

    2. Detection

    • 触发链接后,可以将其保存以用于恶意活动的进一步分析和检测,例如传播恶意软件或诈骗。
    • 检测的目的:捕获链接、重定向链(网页重定向中的URL)和登陆页面(最终的网页)。然后可以使用各种方法进一步分析链接、重定向链和登录页面的内容。

    2.1 Redirection chains

    • 广告从一个链接重定向到另一个链接,直到最终到达登陆页面。
    • 在非广告链接中也可以观察到重定向链。
    • 重定向可使用几种技术来执行:包括HTTP 301/302状态头、HTML元标签和JavaScript级别。
    • 此外,我们发现某些广告网络(如谷歌广告),显然使用基于时间的检查,以减少点击欺诈的可能性。其结果是链接必须实时启动才能获得重定向消息。
    • 为了确保我们的方法准确地遵循重定向链,而不考虑所使用的重定向技术,就像实际用户一样使用一个工具化的web浏览器来遵循该链。
    • 做了什么
      • 实现了一个在虚拟化执行环境中运行的自定义的浏览器,以便将广告完全真实地加载到浏览器中,从而可以完全捕获重定向链。
      • 这个浏览器的实现基于android中提供的webview。
      • 启用了Javascript并调整了一些其他选项,它的行为完全像一个web浏览器。
      • 还附加了相关的部分来记录加载到其中的每个URL(包括重定向的URL),同时允许任何重定向发生。
    2.2 Landing pages
    • android中的登录页面(Landing pages)或者重定向链中的最终网址,可能包含导致应用程序下载的链接。
    • 恶意登录页面可能诱使用户下载木马应用程序。
    • 做了什么
      • 将登陆页面加载到一个配置了真实用户代理和与移动设备相对应的窗口大小的浏览器中,因此浏览器看起来像是安卓系统上的Chrome浏览器。
      • 然后收集登陆页面上的所有链接,点击每个链接查看是否有文件下载。
      • 模拟在浏览器中加载的页面上的点击,确保在出现基于Javascript的事件时可以准确地找到并正确点击链接。
    2.3 File and URL scanning
    • 可以通过各种方式对收集到的URL和文件进行恶意分析。
    • 做了什么:本文没有开发自己的分析,而是使用来自VirusTotal的URL黑名单防病毒软件(VirusTotal汇总了50多个黑名单和类似数量的防病毒软件)。
      • 通过VirusTotal提供的URL黑名单可以扫描收集到的每个URL(登录页面或者重定向链中涉及到的任何URL),其中包括如Google Safebrowsing、Websense Threatseeker、PhishTank等黑名单。
      • 通过登录页面上下载收集到的文件将通过VirusTotal上提供的防病毒软件(antiviruses)进行扫描。
    • 防病毒软件和黑名单(Antivirus systems and blacklists)有可能会误报,为最大程度减少这种影响,使用防病毒软件之间的协议来降低误报率:认为一个URL或文件只有在被至少三个不同的黑名单或防病毒软件标记时才是恶意的。

    3. Provenance

    • 一旦检测到恶意事件,有必要对相关方做出正确的判断,以便追究相关方的责任并采取适当的措施。在该系统中,使用两个方面作为起源(或出处)的一部分。
      • Redirection chain:重定向链:它已经作为检测组件的一部分被捕获。重定向链可用于识别如何到达最终的登录页面:如果登录页面包含恶意内容,则可以识别拥有指向登录URL的URL的各方。
      • Code-level elements:代码级元素:应用本身可以包括来自多方的代码(例如主要应用开发者以及来自各种广告网络的广告库)。为了从一个应用程序启动另一个应用程序,安卓使用所谓的意图(intents)。通过向系统提交带有特定参数的意图,应用程序可以在系统的web浏览器中打开URL。修改系统以记录特定的意图,这些意图表示URL启动以及提交该意图的代码那一部分(启动代码所在的Java类)。这能够确定应用程序中的哪些代码启动了恶意的网址
    • 重要的是要确定作为来源(出处)一部分捕获的代码类的所有者:他们属于应用程序开发人员还是广告库,如果他们属于广告库,是哪一个?为了做到这一点,执行了一次识别流行的广告库及其相关的广告网络的任务。

    4. Ad Library Identification

    • 利用广告赚钱的应用程序通常与广告网络合作,并从中嵌入称为广告库的代码,以便显示和管理广告。
    • 这里的目标是全面识别参与android生态系统的广告网络及其相关的广告库。【这种识别对于自动分类恶意活动是广告的结果还是应用程序开发人员的责任非常重要。】
    • 基于代码中嵌入的广告库,采用了两种系统方法来进行识别:
    4.1 Approach 1
    • 方法一依赖的事实:一个广告网络可能会被许多应用程序使用。因此在使用广告网络的所有应用程序中都可以找到通用的广告库代码。android应用程序的本机编程平台是Java,Java包提供了在名称空间中组织相关代码的机制。广告库本身有可以作为其识别签名的软件包。
    • 做了什么
      • 在第一种方法中,从数据集中的所有应用程序中收集程序包,并创建程序包层次结构以及每个程序包的出现频率。
      • 对程序包进行分类(排序),然后手动搜索最常见的程序包来识别广告库。例如,排序后,像com.facebook和com.google.ads这样的包会出现在顶部。
      • 然后,基于先验知识或在web上手动搜索有关程序包的信息,确定每个包的性质(即它是否构成一个广告库)
    • 这种方法取决于假设广告库程序包不会被混淆。大多数情况都是这样,顶级程序包可以很好地识别大多数广告库。但是Airpush是一个众所周知的广告网络,它混淆了它的广告库,以至于它不再能被包名识别。
    4.2 Approach 2
    • 当我们达到几百个频率时,上面的方法变得很麻烦,因为许多非广告包也有这样的频率。替代方法可以全面识别广告库,而不依赖于这些广告库出现的频率。
    • 方法二依赖的事实:主要的应用程序功能仅与广告库的功能松散耦合(相互联系却又彼此保持独立)。
    • 做了什么
      • 使用zhou等人描述的技术来检测应用程序中的松散耦合组件。
        • 耦合实际上是根据诸如字段引用、方法引用和跨类的类继承等特征来度量的。理想情况下,一个广告库的所有软件包将被分组到一个组件中。实际上,这种情况并不总是会发生,而且可能会发生本应在不同组件中的类最终出现在相同组件中的情况。然而,误差是可以容忍的,并且可以手动分析。
      • 采用聚类技术,进一步简化手动分析:创建了一组在应用程序组件中调用的android API。这组API形成了组件的签名。将这些API映射为整数以实现有效的集合计算。
        • 基于此,具有相同版本的广告库实例具有匹配的API集。对于不同的版本,集合将相似但不相同。
        • 对从所有应用程序中提取的组件进行这个分析,然后使用Jaccard距离来计算API集之间的差异。如果它低于某个阈值(这里使用0.2),则将组件放在同一个群集中。因此,不同广告库的程序包最终位于不同的群集中,然后群集可以很容易地映射到广告库。
    • 该方法不受字典混淆的影响。检测到了混淆的Airpush程序包,所有这些包最终都聚集在几个群集中。
    4.3 Results
    • 使用上述两种方法,在数据集中识别出来201个广告网络。【这是所确定的最多的广告网络】

    实施细节

    • Python 实现了大部分系统。
    • UI Exploration:
      • 利用了 AppsPlayground 工具的源代码。【但是该工具的现有版本无法在当前版本的android系统上运行,因此重新实现了该系统,使其能够在当前的android版本上以与AppsPlayground论文中所述相同的探索式方法运行。】
      • 没有使用HiearchyViewer来获取应用程序的当前用户界面层次,而是使用了基于android可访问性服务的 UIAutomator 。这对执行速度产生了重大而积极的影响。
      • 用于按钮检测的图形算法由OpenCV库提供,并在重复测试后选择合适的阈值。
    • 为了提高动态分析的速度,利用了 KVM 加速虚拟化。为此使用可以在x86架构上运行的Android images。大约70%的安卓应用程序没有本机代码(native code),因此可以在这样的目标上正常运行。
    • 对于触发后分析,整个框架是通过 Celery 来管理的,它为作业管理提供了在分布式环境中部署的能力。
    • 实现中,应用程序UI Exploration以及用真正的浏览器记录重定向链是同时发生的。这一阶段完成,任何记录的重定向链都将通过REST API排队,并与有关应用程序和负责触发导致重定向链的意图的部分代码的信息一起进入到 Celery 管理的队列中。
    • 将任务从队列中拉出以对登录页面进行进一步分析,并使用 VirusTotal 扫描文件和URL。
    • 整个系统有适当的重试和超时机制,可以运行数月,而不需要大量的人力。
    • 所有结果分析数据都存储在MySQL和MongoDB数据库中。
    • 使用 WatirSelenium Webdriver 框架在 Chromium web 浏览器上实现了对登陆页面或重定向链中最终URL分析。
    • 使用 WatirWebdriver 编写浏览器操作脚本,用于自动加载网页、单击链接、自动下载单击链接时可用的内容,以及在单击链接后加载新页面时返回原始页面。所有的处理都是使用 Xvfb显示服务器 没有任何GUI完成的,该服务器是不显示屏幕输出的X服务器实现。
    • 应用程序在虚拟化环境中最多运行五分钟,平均运行时间不到两分钟。
    • 触发后分析(尤其是对登录页面的分析),最多允许运行15分钟。之所以允许运行这么长的时间,是因为我们的爬虫可能遍历许多链接,并且每个链接可能具有复杂的重定向机制,这些机制可能仅在短暂的等待之后触发。

    结果

    应用程序收集

    • 应用程序数据集包含来自Google play的429534个apps和四个中国android应用程序商店( 91, Anzhi, AppChina, and Mumayi)的422505个apps。
    • 由于前面提到的实现原因,整个可用的apps数据集由略超过600000个apps组成。

    部署方式

    • 在美国西北大学的位置运行来自Google play的应用程序(仅美国可提供的应用程序)
    • 中国浙江大学的位置运行来自中国应用商店的应用程序。
    • 运行应用程序的位置很重要,因为很多广告(构成我们正在研究的app-web interaction的大部分)都是基于位置的。在一个位置看到的广告可能不会在另一个位置显示。
    • 部署持续运行,几乎不需要人工干预,只有当需要更新系统来修复错误或添加功能时,才需要重新启动。

    总体调查结果

    • 美国部署中
      • 总共记录了超过100万次的应用程序到web链接的启动。
      • 总共检测到了来自64个唯一域的948个恶意URL。
      • 收集了468个独特的应用程序(来自web,Google play之外),其中271个被发现是恶意的【有244个来自反病毒骗局(antivirus scam)】
      • 作者发现从web下载的apps(Google play之外的)中有六分之一是恶意的。【不包括Google play上的应用程序】
      • 分别对这些应用程序进行了统计:有433,000个Google play登陆URL【即play.google.com域名的http网址或市场方案网址(以“市场://”开头)】。这些Google play登陆URL在Google play上产生了19,000多个应用程序,在VirusTotal中约有5%被标记为恶意的。根据对防病毒标签的手动检查,所有这些似乎都是广告软件
    • 中国部署中
      • 总共记录了415,000次。【这与应用程序不是直接对应的:一些应用程序可能会导致多次启动,而另一些应用程序可能不会导致任何启动。】
      • 检测到了来自139个唯一域的1475和恶意URL。
      • 收集了1097个独特的文件,其中435个是恶意的【其中102个文件来自反病毒骗局(antivirus scam)】。
    美国部署 中国部署
    • 没有显示未导致恶意软件下载的库。
    • Tapcontext恶意软件数量太多,此处未显示。
    • “others”栏通过未嵌入广告的web链接显示下载内容。
    • 中国部署中的更高多样性和更高数量的恶意下载都值得注意。【这可能是因为北美的android生态系统是以Google Play为中心的,在它之外的应用程序下载得很少。然而,中国更依赖于web和第三方android应用商店。】
    美国部署重定向链长度 中国部署重定向链长度
    • 随着重定向链的长度增加,两条曲线越来越接近,也就是说,当它们更长时,恶意链的比例更大。

    主要代码分析

    • 由于自己对python的不熟悉导致在复现过程中运行程序有困难,所以这里对主要程序进行逐句分析。

    1. driver5.py代码

    #!/usr/bin/env python
    
    import sys
    import os
    import subprocess
    import logging
    from threading import Thread
    
    # plg是AppsPlayground的库,以下涉及到的代码在后面进行分析
    
    # plg.utils.androidutil包含各种帮助实用程序,用于启动带有“adb”的命令
    import plg.utils.androidutil as au
    
    # plg.utils.logcat包含获取“logcat”消息的代码。
    import plg.utils.logcat as lc
    
    # plg.metadata从应用程序中提取元数据
    # 元数据被定义为:描述数据及其环境的数据
    from plg.metadata import getmetadata
    
    # plg.explore.explore提供运行应用程序显示地面的主要入口点
    from plg.explore import explore
    
    # plg.settings包含整个代码中使用的各种常量。有些已经不用了。
    # 这里:LAUNCHER_PKG = 'com.android.launcher'
    '''目前还不知道这个的作用是什么'''
    from plg.settings import LAUNCHER_PKG
    
    
    # plg.androdevice:带有视图层次结构的连接套接字和 monkey 的 android 设备
    # Monkey是运行在模拟器或设备上的一个程序,用来伪随机地模拟点击和触摸等用户事件
    # run_monkey():这个monkey设备可以防止进入外部包,还可以检测ANRs(Application Not Responding,应用程序无响应)
    from plg.androdevice import run_monkey
    
    
    MAX_EMULATOR_WAIT = 120 # in seconds
    
    
    # 终止由启动模拟程序返回的进程
    def finish(device):
        print('killing', device, file=sys.stderr)
        au.killemulator(device)
    
    
    
    # *args 是一个元组tuple(元组与列表类似,不同之处在于元组的元素不能修改)
    # 可以接受序列的输入参数。当函数的参数不确定时,可以使用 *args
    def main(avd, app, *args):
        emu_cmd = ['emulator64-x86', '@{}'.format(avd)] #启动模拟器的命令
        
        # 如果输入在args中的前两个元素是:-system filepath
        # 这个是用来指定初始系统文件。提供文件名,以及绝对路径或相对于工作目录的路径
        # 如果不使用此选项,则默认为系统目录中的 system.img 文件
        if args and args[0] == '-system': 
            emu_cmd.extend(args[:2]) # 顾头不顾尾,取前两个元素放到emu_cmd列表后
            args = args[2:] # args中第三个元素一直到最后重新赋值给args元组
        
        
        #对于计算机上运行的第一个虚拟设备实例,默认值为 5554
        # 在特定计算机上运行的第一个虚拟设备的控制台使用控制台端口 5554 和 adb 端口 5555。
        # 后续实例使用的端口号渐增 2,例如 5556/5557、5558/5559 等。
        # 范围是 5554 到 5682,可用于 64 个并发虚拟设备。 
        port = 5554
        
        # 如果args内容不为空,就把args的第一个值赋给port,之后的给 *args
        if args:
            port, *args = args
        
        log = 'log.txt'
        
        # 如果args内容不为空,就把args的第一个值赋给log,之后的给 *args
        if args:
            log, *args = args # we will ignore other args
        
        
        #启动模拟器的命令;
        #extend()函数用于在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)
        #-no-snapshot-save:执行快速启动,但在退出时不保存模拟器状态
        #-port:设置用于控制台和 adb 的 TCP 端口号
        #-m 512:是给客户机分配 512MB 内存
        #-enable-kvm:利用 KVM 来访问硬件提供的虚拟化服务
        emu_cmd.extend(['-no-snapshot-save', '-port', str(port), '-qemu', '-m',
            '512', '-enable-kvm']) 
        
        # the x86 emulators need help finding some libraries
        
        
        
        # environ 是一个字符串所对应环境的映像对象
        #把 android-sdk/tools/lib 添加到 LD_LIBRARY_PATH 路径中
        emu_env = os.environ.copy()
        emu_env['LD_LIBRARY_PATH'] = ('/home/ubuntu/Android/sdk/'
            'tools/lib')
        
        
        device = 'emulator-{}'.format(port)
    
        au.init() # 杀死当前存在的模拟器
        # file=sys.stderr:将当前默认的错误输出结果保存为 file
        print('launching', device, file=sys.stderr)
        
        # subprocess.Popen 类来处理基本的进程创建和管理
        # 开始执行命令
        subprocess.Popen(emu_cmd, env=emu_env)
        
        
        ''' Python的异常处理机制的语法结构: 
        try:
            <语句>
        except <name>:
            <语句>  #如果在try部份引发了名为'name'的异常,则执行这段代码
        else:
            <语句>  #如果没有异常发生,则执行这段代码
        '''
        try:
            # 等待设备上线
            au.waitfordevice(device, timeout=MAX_EMULATOR_WAIT)
        except subprocess.TimeoutExpired:
            # 保留一个“timeout”用于错误处理
            # 当想要自己的模拟器启动,它实际上可能并没有启动
            print('time out expired while waiting for', device, file=sys.stderr)
            raise # 该语句引发当前上下文中捕获的异常
        
        
        # 从 time 模块中引入sleep函数
        # 使用sleep函数可以让程序休眠(推迟调用线程的运行)
        from time import sleep
        #sleep(300)   # 休眠300秒
        # getadbcmd():帮助函数,返回命令 adb -s emulator-5554 install app
        # 这个命令的意思是:在emulator-5554模拟器上安装app(.apk)
        install_cmd = au.getadbcmd(['install', app], device)
        
        
        try:
            # 子进程执行install_cmd中的命令,并将其输出形成字符串返回
            # 如果子进程退出码不是0,抛出subprocess.CalledProcessError异常
            # 将stderr参数设置为subprocess.STDOUT,表示将错误通过标准输出流输出
            subprocess.check_output(install_cmd, stderr=subprocess.STDOUT)
        except subprocess.CalledProcessError as e:
            # join():用于将序列中的元素以指定的字符(这里应该是空格)连接生成一个新的字符串
            # splitlines():按行分割字符串,返回值是个列表
            info = 'install-failed ret:%d %s' % (e.returncode,
                    b' '.join(e.output.splitlines()))
            print('info', file=sys.stderr)
            finish(device) # 终止由启动模拟程序返回的进程
    
    
    
        # config logcat配置日志
        lc_file = log
        
        # 执行命令:adb -s emulator -5554 logcat -c
        # -c:清除屏幕上的日志
        lc.clearlogcat(device)
        
        # 
        lc.logcat(lc_file, device) # file open/close is done by callee
        
        
        # 从应用程序中提取元数据
        metainfo = getmetadata(app)
    
        # launch monkey to prevent straying and deal with ANRs
        # 触发monkey来防止走失和避免ANRs
        ''' 使用Thread类实现多线程:
        1、创建Thread类的实例;
        2、通过Thread类的构造方法的target关键字参数执行线程函数;通过args关键字参数指定传给线程函数的参数。
        3、调用Thread对象的start方法启动线程。
        其中:
        target: 要执行的方法
        args: 要传入方法的参数
        '''
        # 这里run_monkey()执行的命令是:adb -s emulator-5554 shell monkey --port 1080 -p metainfo['name']
        # --port 端口号:为测试分配一个专用的端口号
        # -p:用此参数指定一个或多个包(Package,即App)。指定包之后,monkey将只允许系统启动指定的APP,如果不指定包,将允许系统启动设备中的所有APP.
        # 该命令的意思是:在emulator-5554模拟器上,启动指定的应用程序
        t = Thread(target=run_monkey, args=(device, metainfo['name']))
        t.daemon = True #daemon被设置为True时,如果主线程退出,那么子线程也将跟着退出
        t.start() #启动线程
    
        print('begin exploring')
        # explore():AppsPlayground的主要入口点
        explore(device, metainfo)
        print('finish exploring')
        finish(device) # 终止由启动模拟程序返回的进程
    
    
    if __name__ == '__main__':
        # 输入的参数中不能有 help、-h、-help
        if ('help' in sys.argv or '-h' in sys.argv or '-help' in sys.argv or
                len(sys.argv) < 3):
            print('usage:', sys.argv[0],
                    'avd app [-system <system.img>] [port [log]]')
            #sys.exit(0):表示正常退出
            #sys.exit(2):数值2为不正常,可抛异常事件供捕获
            sys.exit(2 if len(sys.argv) < 3 else 0)
            
        # logging 模块用于打印日志
        # logging.basicConfig()函数实现打印日志的基础配置
        # 参数level: 设置日志级别,默认为logging.WARNING
        # 参数stream: 指定日志的输出流,默认输出到sys.stderr
        logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
        
        # 将输入的参数传入到main函数
        main(*sys.argv[1:])
    
    

    2. plg.utils.androidutil中的init()函数:

    # 杀死任何已存在的模拟器
    def init(logfile=None):
        if not logfile:
            logfile = sys.stderr  # 将当前默认的错误输出结果保存为logfile
        print('(re)starting adb server', file=logfile)
        killserver()
        startserver()
        #sleep(10) # let adb start
        print('killing any emulators already present', file=logfile)
        for device in getdevices():
            killemulator(device)
    

    3. plg.utils.androidutil中的getadbcmd()函数:

    def getadbcmd(args=None, device=None):
        ''' helper function:
            args - arguments excluding adb and device'''
        preargs = [ADB]
        if device:
            #strip()方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。
            device = device.strip() 
            if device:
                preargs += ['-s', device]
        if not args:
            return preargs
        return preargs + args
    
    

    4. plg.utils.androidutil中的runadbcmd()函数:

    def runadbcmd(args, device=None):
        # 运行由args参数提供的命令
        # 如果命令行执行成功,check_call返回返回码 0
        # 否则抛出subprocess.CalledProcessError异常
        return subprocess.check_call(getadbcmd(args, device))
    

    5. plg.utils.logcat中的clearlogcat()函数:

    def clearlogcat(device=None):
        return androidutil.runadbcmd(['logcat', '-c'], device)
    

    6. plg.utils.logcat中的logcatlines()、_logcat()、logcat()函数:

    
    def _enqueue_output(out, q):
        for line in iter(out.readline, b''):
            q.put(line)
        out.close()
    
    
    def logcatlines(device=None, args=''):
        # cmd的值是:adb -s emulator-5554 logcat
        # 该命令的意思是:查看日志输出
        cmd = ' '.join(androidutil.getadbcmd(args, device)) + ' logcat ' +args
        
        '''
        这里使用了pty模块,pty模块定义了处理伪终端概念的操作:启动另一个进程,并能够以编程方式读写其控制终端。
        因为伪终端处理是高度依赖于平台的,所以只有在Linux下才有代码可以做到这一点。
        pty.openpty():
        Open a new pseudo-terminal pair, using os.openpty() if possible, 
        or emulation code for generic Unix systems. Return a pair of file 
        descriptors (master, slave), for the master and the slave end, respectively.
        打开一个新的伪终端对,分别为主机和从机端返回一对文件描述符(主机、从机)。
        '''
        logmaster, logslave = pty.openpty()
        
        # subprocess模块中的Popen类来创建进程,并与进程进行复杂的交互
        # 参数stdout, stderr分别表示子程序的标准输出、错误句柄。
        # 参数shell设为True,指定的命令会在shell里解释执行
        # 参数close_fds设为True,执行新进程前把除了0、1、2以外的文件描述符都先关闭
        logcatp = subprocess.Popen(cmd, shell=True,
                stdout=logslave, stderr=logslave, close_fds=True)
        
        # os.fdopen()方法:用于通过文件描述符 fd 创建一个文件对象,并返回这个文件对象
        # 这里logmaster文件描述符是一个小整数
        stdout = os.fdopen(logmaster)
        
        
        q = Queue()
        
        
        t = Thread(target=_enqueue_output, args=(stdout, q))
        t.daemon = True
        t.start()
        while logcatp.poll() is None:
            try:
                yield q.get(True, 1)
            except Empty:
                continue
    
    
    def _logcat(device, fname, logcatargs):
        with open(fname, 'w') as f:
            for line in logcatlines(device, logcatargs):
                f.write(line)
                f.flush()
    
    
    def logcat(fname, device=None, logcatargs=''):
        ''' run logcat and collect output in file fname.'''
        # 通过Process类创建进程,基本使用与 Thread() 类似
        proc = Process(target=_logcat, args=(device, fname, logcatargs))
        proc.start()
    
    

    7. plg.metadata中的getmetadata()函数:

    # 从应用程序中提取元数据
    def getmetadata(apk):
        # 在子进程执行命令,以字符串形式返回执行结果的输出
        # 如果子进程退出码不是0,抛出subprocess.CalledProcessError异常
        # 执行的命令是:aapt d badging apk,用来查看apk版本及其相关信息
        aaptout = subprocess.check_output(['aapt', 'd', 'badging', apk])
        
        # 创建了一个存放元数据的字典
        data = {}
        data['uses-permission'] = []
        data['uses-feature'] = []
        data['uses-library'] = []
        data['launchable'] = []
        
        # splitlines():按照行('
    ', '
    ', 
    ')分隔,返回一个包含各行作为元素的列表
        # 这里没有参数,相当于keepends为 False,不包含换行符
        # 直接对file对象使用for循环读每行数据
        for line in aaptout.splitlines():
            line = line.decode() # 解码(将字节码转换为字符串,将比特位显示成字符)
            
            # split():通过指定分隔符 ' 来对字符串进行切片
            tokens = line.split("'")
            
            # startswith():用于检查字符串是否是以指定子字符串开头,如果是则返回 True,否则返回 False
            if line.startswith('package:'):
                data['name'] = tokens[1]
                data['versionCode'] = tokens[3]
                data['versionName'] = tokens[5]
            elif line.startswith('uses-permission'):
                data['uses-permission'].append(tokens[1])
            elif line.startswith('sdkVersion'):
                data['sdkVersion'] = tokens[1]
            elif line.startswith('targetSdkVersion'):
                data['targetSdkVersion'] = tokens[1]
            elif line.startswith('uses-feature'): # both required and not required
                data['uses-feature'].append(tokens[1])
            elif line.startswith('uses-library'): # both required and not required
                data['uses-library'].append(tokens[1])
            elif line.startswith('application:'):
                data['app-label'] = tokens[1]
                data['app-icon'] = tokens[3]
            elif line.startswith('launchable activity') or line.startswith(
                    'launchable-activity'):
                data['launchable'].append(dict(name=tokens[1],
                    label=tokens[3], icon=tokens[5]))
        return data
    
    

    8. plg.androdevice中的run_monkey()函数:

    def run_monkey(name=None, pkg=None):
        '''
        This monkey process prevents going to outside packages and also detects
        ANRs. In future we may do something else for these two functionalities. In
        case of ANR, kill emulator so that this worker eventually crashes.
    
        Parameters
        ----------
        name: str
            the device name such as 'emulator-5554'
        pkg: str
            the application package outside which plg should not go
        '''
        cmd = ['shell', 'monkey', '--port', '1080']
        if pkg:
            cmd.extend(['-p', pkg])
        runadbcmd(cmd, name)
        killemulator(name)
    

    9. plg.explore中的explore()函数:

    def explore(dev, appinfo):
        ''' The main entrypoint for AppsPlayground.
    
        Parameters
        ----------
        dev:
            can be a string like 'emulator-5554' or an `AndroidDevice` instance
        appinfo:
            the metadata for the app, such as one derived from
            plg.metadata.getmetadata()
        '''
        if type(dev) is AndroidDevice:
            androdev = dev
            dev = dev.name
        else:
            dev = str(dev)
            androdev = AndroidDevice(dev)
        DevState(dev, androdev, appinfo).explore()
    

    复现

    环境准备

    1. 配置jdk

    • 将下载下来的 jdk-8u241-linux-x64.tar.gz 进行解压
    sudo tar zxvf ./jdk-8u241-linux-x64.tar.gz
    
    • 设置环境变量
    sudo vim /etc/profile
    
    • 在其中添加:
    #set Java environment
    export JAVA_HOME=/home/ubuntu/jdk1.8.0_241
    export JRE_HOME=$JAVA_HOME/jre
    export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
    export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
    
    • 生效环境变量配置,命令行输入:
    source /etc/profile
    
    • 使用命令 java -version 检查Java是否已经安装在Ubuntu上

    2. 安装Android studio以及配置sdk和adb

    • 下载安装包https://www.androiddevtools.cn/
    • 安装软件:解压压缩包,运行studio.sh脚本,完成剩下的安装。这个要在进入root模式之后进行:
    sh /home/ubuntu/android-studio/bin/studio.sh
    
    • 按照提示,一步一步安装。
    • 之后会自动下载sdk文件,我的在/root/Android/Sdk下;
    • 配置环境变量:
    sudo vim /etc/profile
    
    • 在其中添加:
    export ANDROID_HOME=/root/Android/Sdk
    export PATH=${PATH}:${ANDROID_HOME}/tools
    export PATH=${PATH}:${ANDROID_HOME}/platform-tools
    export PATH=${PATH}:${ANDROID_HOME}/build-tools 
    
    • 生效环境变量配置,命令行输入:
    source /etc/profile
    
    • 检查是否配置成功: android -hadb

    • 这里出现了问题
    • 原来,在Android Studio官网的sdkmanager页面中,明确提到了在Android SDK Tools25.2.3及以后提供了sdkmanager command-line tools 以及舍弃android命令.
    • 参考的解决方法
    • 我这里选择下载了android-sdk_r24.4.1-linux.tar,然后解压,复制其中的tools目录,覆盖原来的tools目录。
    • 然鹅还是不行。。。。算了先回复成原来的吧。。
    • 最后下载https://dl.google.com/android/repository/tools_r25.2.3-linux.zip,然后直接把文件拖进sdk文件,直接覆盖,解决啦哈哈哈哈!

    3. 安装KVM

    What's KVM?

    KVM实际是linux内核提供的虚拟化架构,可将内核直接充当hypervisor来使用。KVM需要处理器硬件本身支持虚拟化扩展,如intel VT 和AMD AMD-V技术。KVM自2.6.20版本后已合入主干并发行,除此之外,还以模块形式被移植到FreeBSD和illumos中。除了支持x86的处理器,同时也支持S/390,PowerPC,IA-61以及ARM等平台。

    工作原理

    KVM包含一个内核模块kvm.ko用来实现核心虚拟化功能,以及一个和处理器强相关的模块如kvm-intel.ko或kvm-amd.ko。KVM本身不实现任何模拟,仅仅是暴露了一个/dev/kvm接口,这个接口可被宿主机用来主要负责vCPU的创建,虚拟内存的地址空间分配,vCPU寄存器的读写以及vCPU的运行。有了KVM以后,guest os的CPU指令不用再经过QEMU来转译便可直接运行,大大提高了运行速度。但KVM的kvm.ko本身只提供了CPU和内存的虚拟化,所以它必须结合QEMU才能构成一个完整的虚拟化技术。

    What's QEMU-KVM?

    QEMU-KVM,是QEMU的一个特定于KVM加速模块的分支,里面包含了很多关于KVM的特定代码,与KVM模块一起配合使用。
    目前QEMU-KVM已经与QEMU合二为一,所有特定于KVM的代码也都合入了QEMU,当需要与KVM模块配合使用的时候,只需要在QEMU命令行加上 --enable-kvm就可以。

    • 执行以下命令以验证服务器的硬件是否支持虚拟化。如果输出结果大于 0,就意味着支持虚拟化。
    egrep -c '(vmx|svm)' /proc/cpuinfo
    
    • 在linux终端安装东西的时候都要 sudo apt-get update 一下,然后安装KVM依赖:
    sudo apt-get install qemu-kvm
    sudo apt-get install qemu
    sudo apt-get install virt-manager
    sudo apt-get install virt-viewer 
    sudo apt-get install libvirt-bin 
    sudo apt-get install bridge-utils
    
    • 验证下,终端键入 kvm-ok,出现下面的界面,就说明已经装好了
    • 终端键入 sudo virt-manager ,可以进入图形界面。
    • 配置KVM:添加用户到kvm,libvirtd组,打开Terminal终端,输入:(例如你的用户名为xxx)
    sudo adduser xxx kvm
    sudo adduser xxx libvirtd
    

    • 可以使用以下命令测试安装是否成功:
    virsh list --all
    

    4. 配置aapt

    aapt 为 Android Asset Packaging Tool , 在SDK的build-tools/目录下. 该工具可以查看, 创建, 更新ZIP格式的文档附件(zip, jar, apk). 也可将资源文件编译成二进制文件。

    • 配置环境变量:
    sudo vim /etc/profile
    
    • 在其中添加:
    # Add Android build-tools AAPT variable
    export AAPT_HOME=/root/Android/Sdk/build-tools/29.0.3
    export AAPT_HOME
    export PATH=$PATH:$AAPT_HOME
    
    • 生效环境变量配置,命令行输入:
    source /etc/profile
    
    • 使用命令 aapt 检查:

    5. Add android-sdk/tools/lib to LD_LIBRARY_PATH.

    • 在终端下输入 sudo vim /etc/profile ,添加
    export LD_LIBRARY_PATH=/root/Android/Sdk/tools/lib:$LD_LIBRARY_PATH
    
    • 生效环境变量配置,命令行输入:
    source /etc/profile
    
    • 然后再输入 echo $LD_LIBRARY_PATH 即会显示:

    6. 配置emulator

    • 在配置abd环境的时候其实就已经配置好这个了,它在sdk目录下的tools文件目录下。
    • 但是我发现这个命令只能在root权限下运行。。

    程序运行

    1. 检查试验环境

    2. 创建AVD

    • 在命令行使用命令 android sdk 打开 Android SDK Manager ,然后选择安装ABI,注意:下载时必须下载带有system image的项目,不然创建AVD时就会报如下的错误。参考

    • 添加之后可以运行啦!

    • 运行的过程中会出现类似以下这样的问题,我就照着在相应的位置(运行卡住的模块后面)添加对应的模块,一共添加了如下九个模块(猜测可能是因为修改了android的平台版本编号导致)。








    • 使用命令 android list avd 查看新创建的AVD,发现错误:

    • 查了好多资料,说这个问题的原因是Android Studio安装和Sdk的安装位置不在同一个盘符下面,还有就是ANDROID_AVD_HOME这个环境变量要是SDK目录,然后我修改了环境变量,整体的环境变量如下:

    • 然鹅。。不行。。。。

    • 创建过程中出现的问题如下,发现有很多 EOF 和 TIMEOUT 的字样,查阅资料,分析异常:

    • TIMEOUT 异常:如果子程序没有在指定的时间内生成任何 output,那么 expect()read() 都会产生 TIMEOUT 异常。超时默认是 30s,可以在 expect()spawn 构造函数初始化时指定为其它时间。若想让 expect()read() 忽略超时限制,即无限期阻塞住直到有 output 产生,设置 timeout 参数为 None。

    • 原来的代码如下:

    p = pexpect.spawn('android create avd -n {} -t android-17 --snapshot'
                ' --sdcard 200M --abi x86'.format(sys.argv[1]), timeout=2)
    
    • 这个等待 2s 是不是有点短,试着改成 None 之后,居然成功了(真的是等了好久才创建出来),热泪盈眶o(╥﹏╥)o

    • 与KVM模块配合,通过命令启动虚拟机(这里注意提权,不提权打不开):
    emulator64-x86 -avd y -no-snapshot-load -qemu -m 512 -enable-kvm
    
    • Snapshot的意思是“快照”:能够保存AVD关闭时的状态,然后再下一次启动的时候显示上一次关闭时的状态,类似缓存机制。
    命令行选项 说明
    -no-snapshot-load 阻止模拟器从快照存储加载 AVD 状态。执行完整启动。执行冷启动,并在退出时保存模拟器状态。
    -no-snapshot-save 如果可能,执行快速启动,但在退出时不保存模拟器状态。也就是阻止模拟器在退出时将 AVD 状态保存到快照存储,这意味着所有更改都将丢失。
    -m 512 是给客户机分配512MB内存
    -enable-kvm 利用 KVM 来访问硬件提供的虚拟化服务



    • 打开的速度和上次没有使用kvm相比较果然飞快!!!

    3. 安装app并且进行检测

    • 测试一下有没有模块安装的问题:
    • 然后下载想要安装的apk。这里下载了人人影视——moc_steyy_www_184.apk
    • 在命令行输入 python driver5.py y moc_steyy_www_184.apk
    • 启动之前创建好的avd——y,但是总在规定的时间启动不了,出现subprocess.TimeoutExpired的问题:
    • 于是我决定在命令行中使用命令启动(在源代码中把启动app的代码注释掉),因为源代码中命令如下,使用参数 -no-snapshot-save ,整体命令如下所示:
    emulator64-x86 @y -no-snapshot-save -port 5554 -qemu -m 512 -enable-kvm
    
    • 启动成功!
    • 在app安装过程中出现网络连接错误的情况:
    • 但是当我再次打开avd的浏览器发现是可以进行上网的。这里不知道为啥。
    • 还是采取了一下解决方案
    • 但还是app中上不了网。
    • 由于这个app没有其他页面和按钮,于是我打算尝试一下变得app。这次下载的时候我选择了与avd版本一致的app——看动漫app for android 4.2.2 安卓版
    • 继续重复之前的操作。
    • 发现可以正常检测:
    • 系统自动进行按钮检测,并且模仿人工点击,从界面第一页一直到最后一页,随后退出app。
    • 这里遇到一个问题,创建好的avd是可以正常连接网络并且打开网页的,但是随后安装好的app却总是显示无法连接。没有网络连接就无法捕获链接、重定向链(网页重定向中的URL)和登陆页面(最终的网页)。。。。
    • 还有一个问题,就是在检测的最后一步要退出app,一直退不出来。。。
    测试的apk 出现的问题
    泰捷视频v4.2.2安卓版 安装好闪退
    人人影视 无法连接网络,只有一个界面,不能退出
    看动漫v4.2.2 可以进行自动点击,但是无法连接网络,不能退出
    BBC-Hausav4.2.2 直接让下载另一个版本,无法连接网络下载不了
    泰捷视频tv版v4.2.2 直接闪退
    好料V4.2.2 安装不成功
    神豆手机号定位 闪退
    网易云音乐 闪退
    1号店v4.2.2 只有一个界面,退出不了
    慕课安卓版v4.2.2 只有一个界面,退出不了
    安智市场 可以进行自动点击,可以联网,退出不了,最后卡了

    4. 查看结果分析数据

    • 结果分析数据都存储在MySQL和MongoDB数据库中。
    • 但是由于前面实验没有成功,还有问题待解决,就没有进行分析。

    遇到的问题

    问题1

    • 之前装好了anaconda和pycharm,但是我打开pycharm的时候发现没有菜单栏,连settings都找不到。
    • 解决方法:
      • 按Ctrl + Shift + A打开“Action”对话框,键入“Experimental features”,然后按Enter键。
      • 把linux.native.menu选项旁边的复选框的钩子去掉,变成空白选项,应用更改并关闭对话框。
      • 重启PyCharm

    问题2

    • 使用 pip 安装需要的模块的时候,出现下面这样的问题:
    you are using pip version 10.0.1,however version 20.0.2 is available
    You should consider upgrading via the ‘pip install --upgrade pip’ command.
    
    • 使用提示的更新命令:
    pip install --upgrade pip --user
    
    • 更新成功并可以安装所需模块:
    • 在安装urllib3==1.9的时候出现了如下错误,但是最后显示还是安装成功了,不知道会不会对后续的实验有影响,先把图放在这里吧:
    • 错误消息:
    ERROR: requests 2.19.1 has requirement urllib3<1.24,>=1.21.1, but you'll have urllib3 1.9 which is incompatible.
    
    • 在pycharm中添加conda的interpreter时候,出现如下问题:
    • 错误信息:
    /home/ubuntu/anaconda3/lib/python3.7/site-packages/requests/__init__.py:91: RequestsDependencyWarning: urllib3 (1.9) or chardet (3.0.4) doesn't match a supported version!
      RequestsDependencyWarning)
    
    pip install --upgrade urllib3
    pip install --upgrade requests
    
    • 后来又遇到下面问题,重新安装pycharm即可。

    • 我发现在命令行使用命令 pip list 显示所安装好的包之后,在pycharm的解释器环境中找不到对应安装好的包,使用命令 which python 查看ubuntu中安装的python路径,然后就可以找到在命令行安装好的包啦。

    问题3

    • 额。。,居然死机了,鼠标键盘都不动了。可能是我打开的窗口太多了。。
    • Ctrl + Alt + PrtSc (SysRq) + reisub ,再说明白一点,就是按住Ctrl,Alt和PtrSc(SysRq),按住他们的同时你需要按r,e,i,s,u,b 这样就能安全地重启linux

    问题4

    • 在配置完LD_LIBRARY_PATH之后,应该是这个环境变量配置错误,导致把PATH覆盖了(此时的我快崩溃了)
    • 恢复办法如下:
    /usr/bin/sudo vim /etc/profile          (由于找不到sudo,所以必须写全路径,其他命令如果提示找不到,也需要写全路径)
    
    • 在末尾加上:
    export PATH="$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games"
    
    • 执行以下命令生效
    source /etc/profile
    

    学习总结与反思

    本次综合实践选择了有关Android安全方面的实践,也是因为自己的研究方向是这个,和这次攻防的主题比较相近,感觉比较有趣,又贴近日常生活。但是实验难度相对来说比较大,这个实验的环境Linux系统开始我安装在了一个64G的U盘上,把U盘作为系统启动盘来进行实验。但是后来发现启动avd的时候设备空间不够,于是又换成了256G的U盘,虽然实验进行到后面还是有点卡,但是等个十分八分的还是可以的。也是自己对Linux系统和python的不熟悉,还有Android模拟器也是第一次接触。实验复现历经了一个多月吧,这个过程中我经常因为出现无法解决的问题感到头痛,但是灵光乍现解决问题之后也真的是痛快淋漓。虽然最后复现没有成功,还是有很多问题,但是学到知识的快乐丝毫没有减少。感觉自己解决问题的能力也得到了提高,之前在学校遇到问题总是喜欢麻烦别人,现在发现原来自己也是有能力解决各种问题的,哈哈哈哈为自己的成长感到开心。本学期的课程虽然已经结束了,但是这个实验没有解决的问题我还是会继续把他解决掉滴!

    参考资料

  • 相关阅读:
    SSIS -->> Data Type
    SSIS ->> Parameter
    Data Flow ->> Term Lookup
    Data Flow ->> Term Extraction
    Data Flow ->> Pivot
    Data Flow ->> OLE Command
    Data Flow ->> Multicast
    Data Flow ->> Union All
    Data Flow ->> Merge
    LeetCode OJ 118. Pascal's Triangle
  • 原文地址:https://www.cnblogs.com/yangdd/p/13222368.html
Copyright © 2020-2023  润新知