android安全技术点

移动端安全的东西很多,花样也很多,不论是从硬件架构,操作系统,还是其他安全角度来讲,每接触一项新事物,都有可能需要学习一整个生态内的安全知识。
这里整理了一些我自己关于android的一些学习笔记和内容,以铺大饼的形式尽量囊括在入门Android安全所学习过的一些知识点,希望对大家有所帮助。

ps:以下内容没有具体例子的分析,原因是其中的大部分知识点,我都是从其他师傅们的文章和代码一路学习过来的。文章也整理了以下放有我自己学习下来感觉收获较大的资源链接。

总结下来,对于一个平台的逆向工程技术需要掌握的技能大致如下:

  • 操作系统的安全架构
  • 操作系统中可执行文件格式
  • 各类工具的使用
  • 反汇编代码的阅读理解
  • 调试器的使用(单独从工具中列出来以示重要性)
  • 网络抓包工具的使用

本文大致分为以下几个模块:

  • android端的基本安全知识
  • android端的调试逆向
  • android端的漏洞挖掘

android端的基本安全知识

  1. 编译与反编译
  2. 虚拟机,汇编,文件结构
  3. Android系统和程序的启动过程

其实总的来说就是—>逆向工程 orz

编译与反编译

apktool:可以将apk文件反编译生成smali格式的反汇编代码,也可将apktool重新编译生成apk文件。这里提醒一点就是具体厂商apktool使用会涉及到具体的资源包。

学习途径:https://ibotpeaches.github.io/Apktool/documentation/

adb:android sdk自带的命令行工具,用于与设备通信,便于执行各种设备操作

学习途径:官方文档配上awesome系列基本上就够用了

  1. https://developer.android.google.cn/studio/command-line/adb?hl=zh_cn2

  2. https://github.com/mzlogin/awesome-adb

放几条常用指令:

通过此指令可以获取所有包名

  • adb shell pm list packages -f

启动app

  • adb shell am start -n [PACKAGE-NAME]/[ACTIVITY-NAME]

列出所有正在执行app的activity

  • adb shell dumpsys activity

停止应用程序

  • adb shell am force-stop [PACKAGE-NAME]或者adb shell am kill-all [PACKAGE-NAME]

signapk,keytool和jarsigner: 用于给apk签名的工具(在这踩过坑,建议搞清楚),这里放一条我常用的解决办法,两条命令,前者是生成自己的密钥,后者是签名,具体选项代表自查

1
2
- keytool -genkey -keystore test.keystore  -alias test -keyalg RSA -validity 10000
- jarsigner -verbose -keystore test.keystore -signedjar signed.apk unsigned.apk test

jd-gui和dex2jar: 反编译工具,通常用于阅读反编译生成的java代码

当然这些都是些传统的工具,推荐在熟悉这些工具的具体运作方式之后再选择高效的集成工具,比如说jadx,jeb等。

apk的打包流程:

  • 用aapt打包资源文件生成R.java文件
  • 处理aidl文件,生成相应java文件
  • 编译工程源代码,生成相应class文件(重要的一步)
  • 转换所有class文件,并生成classes.dex文件(主要是将java字节码转换为Dalvik字节码)
  • 打包生成APK文件
  • 对APK文件签名
  • 对签名后的APK文件进行对齐处理

APK的文件结构:

整体流程如下:

app的安装途径:系统程序安装,android市场安装,adb工具安装,sd卡安装
app安装过程可以追踪分析android系统程序PackageInstaller中PackageInstallerActivity来去理解,具体内容这里不再展开。

虚拟机,汇编,文件结构

Dalvik虚拟机:其设计的初衷也许是是提高运行效率并规避与oracle的版权纠纷
其特点有:

  1. 体积小,占用内存空间小
  2. dex可执行文件格式
  3. 常量池采用32为索引值
  4. 基于寄存器架构,有一套完整的指令集(同时有些寄存器没有用到)
  5. 提供了对象生命周期管理,堆栈管理,线程管理,安全和异常管理,垃圾回收等功能
  6. android程序都运行在android系统进程里,每个进程对应一个Dalvik虚拟机实例

Dalivik文件结构与java文件结构不同,Dalvik虚拟机通过dx工具对java类文件中常量池进行分解,消除冗余信息后在组合成新常量池

又由于Dalvik虚拟机是基于寄存器架构的,相比于基于栈架构的java虚拟机,数据访问会快很多,同时Dalvik的指令即更加精简,程序的执行速度会快些。
android系统架构图:

可见Dalvik属于Android运行时环境,和核心库共同承担Android应用程序的运行工作
这里有一点需要注意就是以消息通信的角度来看又可以分成另一个架构图,其中我们常关注的是native层和java层

初次之外还有artjvm,可以看这篇博客

Android系统和程序的启动过程:

Android系统启动加载内核后—-> 执行init进程 —-> 启动Zygote进程 —-> 初始化Dalvik虚拟机 —> 启动system_server进入Zygote模式,用socket等待命令 —> Zygote收到命令后fork一个Dalvik虚拟机实例来执行程序入口函数
流程大致如下图:

其中Zygote有三种创建进程的方法:

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

fork后 —> 虚拟机通过loadClassFromDex完成装载(用gDvm.loadedClass全局哈希表存储查询类) —> dvmVerifyCodeFlow对代码检验 —> FindClass查找装载main方法类 —> dvmInterpret初始化解释器并执行字节码流

以上就是Dalvik在程序执行时的要点,还有其涉及到的JIT技术和Dalvik的汇编代码内容繁杂,建议直接阅读官方文档或者相应书籍
其中dex文件主流反汇编工具有BakSmali与Dedexer,详细的dex文件格式内容也建议直接阅读相关资料或源码,这里就出DexFile的数据结构。

1
2
3
4
5
6
7
8
9
10
Struct DexFile{
DexHeader Header;
DexStringId StringIds[stringIdsSize];
DexTypeId TypeIds[typeIdsSize];
DexProtoId ProtoIds[protoIdsSize];
DexFieldID FieldIds[fileIdsSize];
DexMethodId ClassDefs[classDefsSize];
DexData Data[];
DexLink LinkData;
}

图1

模拟器体系结构

关于模拟器体系结构的梳理:https://bbs.pediy.com/thread-255672.htm

这个链接放在这里的原因是作者自己曾经根深蒂固地把arm和android两个概念紧紧的结合在一起了,忽略了android studio创建的模拟器是Intel x86架构的,导致踩过坑。

android端的调试

这里说是调试而不说逆向的原因是因为逆向的内容实在是太多了,入门可以参考《android软件安全与逆向分析》,《android攻防权威指南》以及《漏洞战争》中的部分内容来学习,这里选择调试中重要的内容来介绍。姑且就分为以下两者吧

  • 手动调试
  • 半自动化工具调试

smali的调试

在 smali 语法中,使用的都是寄存器,但是其在解释执行的时候,很多都会映射到栈中。通常每个smali会对应一个类。

编译 - smali2dex

给定一个 smali 文件,我们可以使用如下方式将 smali 文件编译为 dex 文件。

  • java -jar smali.jar assemble src.smali -o src.dex

运行 smali

在将 smali 文件编译成 dex 文件后,我们可以进一步执行
首先,使用 adb 将 dex 文件 push 到手机上

  • adb push main.dex /sdcard/

其次使用如下命令执行

  • adb shell dalvikvm -cp /sdcard/main.dex main

AS + smalidea

  1. 打开设备中需要调试的apk,在cmd中运行命令 :adb shell “dumpsys activity top | grep –color=always ACTIVITY”,就可以看到需要调试apk的包名、主acitivity以及进程号(或者也可以使用adb shell “dumpsys activity activities | grep xxxActivity”)
  2. 使用命令以debug模式启动apk:adb shell am start -D -n 包名/主activity名,然后你的设备可以看到wait for debugger
  3. 再一次运行第一步的命令,获取新的进程号:adb shell “dumpsys activity top | grep –color=always ACTIVITY” 或者运行命令: adb shell “ps | grep 包名”亦可
  4. adb forward tcp:debug端口 jdwp:apk进程号
  5. 以上步骤可以直接在as中logcat里选定调试进程,然后选择Attach Debugger To Android Process
  6. 查看与包名相关JDWP命令:adb shell “ps -t | grep -A 8 包名”
  7. 查看所有JDWP进程命令:adb shell “ps -t | grep -B 6 JDWP”

smali的修改

之前尝试用apk改之理,但是由于版本太老了有较多不方便,所以弃坑,现在我通常是apktool反编译后修改,然后再编译并使用自签名,这样也可以达到修改安装使用的效果

基本原生程序

如elf文件就可以

  • 使用 android_server 的 PIE 版本
  • 利用 010Editor 将可执行 ELF 文件的 header 中的 elf header 字段中的 e_type 改为 ET_DYN(3)。

so 原生程序的调试

其加载方式有system.load和system.loadlibrary两种,前者加载绝对路径,后者加载libs下的so文件,两者都会在内部调用doload函数,其流程大致如下

doload -> nativeload -> 对应到Dalvik_java_lang_Runtime_nativeLoad-> dvmLoadNativeCode加载相应的native code -> findSharedLibEntry(判断是否已经加载了这个库以及是否是对应的class loader) –如没有加载–> dlopen打开 –> si->CallConstructors()初始化 –> 创建表且用dlsym获取对应so文件中 JNI_OnLoad 函数

静态分析 java 层: 没什么好说的,理解程序逻辑去做就好了

静态分析原生层程序基本的过程如下

  • 提取 so 文件
  • ida 反编译 so 文件阅读 so 代码
  • 根据 java 层的代码来分析 so 代码。
  • 根据 so 代码的逻辑辅助整个程序的分析。

在android studio 3.12后已经将ddms移除了,所以官方的建议是

图2

当我们不能使用ddms时意味着我们使用jdb进行转发时无法确定具体的port,有一种方法是加载程序运行时需要的so文件,然后在一些关键函数比如jniString()函数下断,运行apk后,然后attach其进程即可。

当然有时候会需要在jni_load下断,而so文件又被处理或者干脆jni_load被加密了,这时需要pull出来libdvm so文件,参考我们加载方式的流程,直接查找dvmLoadNativeCode,函数中调用dlopen加载so,返回时so已经加载且已经初始化完成,调试下就能找到。

hook

hook的方法很多,但原理就是这么几种,主要有Dalvik,ART,inline,GOT等对象hook,这个有太多大佬写过各种类型的hook了,hook的框架也有许多,展开分析内容太多了,就不再累述了。

Frida hook分为注入进程,直接在源码中修改,以及动态链接三种方式

frida的交互实现大致可以这么理解:默认监听27042端口,对目标进程gadget在启动时进行阻塞,直到实现attach进程或者在使用spawn()后再resume恢复进程,当然这些状态都可以通过配置修改,也可以提前设置好过滤器将特定脚本加载到特定应用中。

由于frida是个轻量级的hook框架,所以还是比较容易添加自己想要的功能,具体请看官网的frida架构图。

frida有一个功能可以为我们生成一个进程而不是将它注入到运行中的进程中,它注入到Zygote中,生成我们的进程并且等待输入。即spawn是注入zygote而attach是注入当前进程。

这里有遇到过的一些小坑,分别是设备检测问题和windows端的编码问题:

https://github.com/frida/frida/issues/1111

https://github.com/rkern/line_profiler/issues/37

android漏洞挖掘

一直想挖android上的漏洞,但真正上手的时候发现自己缺乏很多真实场景的经验和思路,毕竟漏洞挖掘和ctf题差距还是不一般的大(汗),于是就整理了下android上常见的问题,尽可能的去整理些思路出来。

ps:这里有一个点没有涉及,就是关于android端会涉及到较多的抓包分析工作,但无奈我是个二进制菜鸡,就不班门弄斧的推荐了。

我们都知道android四大组件,所以这里的大部分内容来自对瘦蛟舞大佬多年前文章的阅读笔记(汗,果然小白只能玩大佬玩剩下的)

Activity

常见的关注点

  • activity的生命周期
  • launch mode
  • taskAffinity
  • task and activity
  • android:exported
  • android:permission

关键方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
onCreate(Bundle savedInstanceState)
setResult(int resultCode, Intent data)
startActivity(Intent intent)
startActivityForResult(Intent intent, int requestCode)
onActivityResult(int requestCode, int resultCode, Intent data)
setResult (int resultCode, Intent data)
getStringExtra (String name)
addFlags(int flags)
setFlags(int flags)
setPackage(String packageName)
getAction()
setAction(String action)
getData()
setData(Uri data)
getExtras()
putExtra(String name, String value)

activity分为四种,在考虑安全时分别从创建activity和使用activity时考虑

广播接收器既可以在manifest文件中声明,也可以在代码中进行动态的创建,并以调用Context.registerReceiver()的方式注册至系统。

注意关注类型 protectionlevel 权限

Broadcast

广播需要注意广播的对象范围以及持久性带来的影响

关键方法

1
2
3
4
5
6
7
8
9
sendBroadcast(intent)
sendOrderedBroadcast(intent, null, mResultReceiver, null, 0, null, null)
onReceive(Context context, Intent intent)
getResultData()
abortBroadcast()
registerReceiver()
unregisterReceiver()
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
sendStickyBroadcast(intent)

ContentProvider用来存放和获取数据并使这些数据可以被所有的应用程序访问。它们是应用程序之间共享数据的唯一方法;不包括所有Android软件包都能访问的公共储存区域。

Content Provider

关键方法

1
2
3
4
5
6
7
8
9
10
11
public void addURI (String authority, String path, int code)
public static String decode (String s)
public ContentResolver getContentResolver()
public static Uri parse(String uriString)
public ParcelFileDescriptor openFile (Uri uri, String mode)
public final Cursor query(Uri uri, String[] projection,String selection, String[] selectionArgs, String sortOrder)
public final int update(Uri uri, ContentValues values, String where,String[] selectionArgs)
public final int delete(Uri url, String where, String[] selectionArgs)
public final Uri insert(Uri url, ContentValues values)
public abstract void grantUriPermission (String toPackage, Uri uri, int modeFlags) //Grant permission to access a specific Uri to another package, regardless of whether that package has general permission to access the Uri's content provider. 临时授权
public abstract void revokeUriPermission (Uri uri, int modeFlags) //Remove all permissions to access a particular content provider Uri that were previously added with grantUriPermission(String, Uri, int). 移动授权

Service

startService与bindService两者的区别就是使Service的周期改变.由startService启动的Service必须要有stopService来结束Service,不调用stopService则会造成Activity结束了而Service还运行着.bindService启动的Service可以由unbindService来结束,也可以在Activity结束之后(onDestroy)自动结束.

关键方法

1
2
3
4
5
6
7
8
onStartCommand() 
OnBind()
OnCreate()
OnDestroy()
public abstract boolean bindService (Intent service, ServiceConnection conn, int flags)
startService()
protected abstract void onHandleIntent (Intent intent)
public boolean onUnbind (Intent intent)